feat(nova): restructure the game detail window around its artwork - #188
Conversation
Fidelity pass —
|
| Was | Now | |
|---|---|---|
| identity line | Steam · Played 17 min. ago · Action |
STEAM · PLAYED 17 MIN. AGO · ACTION — uppercase, .17em |
| status readout | sentence case | uppercase, .11em, tabular figures |
| primary action | flat colors.accent, no glyph |
accent gradient carrying its bound button |
| secondary actions | bare slabs | hairline-bordered, marked |
| floor | bordered hint container on the artwork | borderless, ✦ POLARIS opposite the back hint |
Figures are tabular because they are measurements and should line up rather than dance. The floor is borderless because a bordered container there would be one more box on a screen whose point is that nothing sits on the artwork in a box.
Focus stays a ring and a tint, never a scale or an offset.
Two guards follow the repaint: the Overview paints the shared hint model itself rather than using the bordered bar, and the action lane became its own composable.
Re-verified: 1,170 unit tests, lintNonRoot_gameDebug -PlintFailOnError=true, both release assemblies, instrumented test on the Retroid, and hands-on with Control Ultimate Edition.
The window opened as a single scroll column stacking ten concerns: artwork, identity, launch controls, three inline sub-sheets, MangoHud status, two insight cards and the whole artwork studio. This makes the landing screen answer one question — do I want to play this — and gives everything else a destination. Overview goes full-bleed. It reuses NovaLibraryCinematicBackdrop, so hero-to-poster fallback, the crossfade and the theme scrims come with it rather than being rebuilt. Curated logo artwork becomes the title treatment at 200x64dp; the same asset was previously crushed into 56%x46% of a 136dp thumbnail inside a card inside a sheet. Below it, one reading order: who the game is, a hairline, what the primary action will do, then what you can do. The launch path loses its modals. onPrimaryLaunch branched three ways and only one of them launched; the other two raised a bottom sheet and a stock AlertDialog over the content. Neither needed porting, because the design already had the right home for each: a choice of where to run belongs in the Launch mode destination, and the preflight review is a statement about the profile, so it expands the status line in place and turns the alert's three buttons into the action lane. That removes the last raw AlertDialog in the detail, which the guards already forbade elsewhere. Artwork takes the whole window rather than a side panel. NovaArtworkStudio opens with a Row of weighted Columns and cannot fold into 60% of a landscape shell; Tune and Launch mode keep the panel. Both destinations lay their glass over an opaque ground. surfaces.panel is sheet glass, scaled again by the user's menu opacity, which is right over a dimmed sheet and wrong over a hero — without a ground beneath it the Overview read straight through Artwork. singleTop, because a fast double-press on a poster otherwise stacks two windows. Session-aware verbs and the playtime gauge are deliberately absent: both need data this window does not yet receive.
The first pass reached for NovaActionButton and NovaControllerHintBar because they carry focus handling, theming and accessibility for free. They also carry the library's visual language, so the screen came out structurally right and visually wrong: a flat accent fill with no button glyphs, sentence-case status text, and a bordered hint container sitting on the artwork. The identity and status lines are instrument readouts now — uppercase, tracked, and with tabular figures, since those are measurements and the digits should line up rather than dance. The primary action carries the button it is bound to on an accent gradient; the rest are quiet, hairline-bordered and marked. The floor is borderless with the Polaris mark opposite the back hint, because a bordered container would be one more box on a screen whose point is that nothing sits on the artwork in a box. Focus stays a ring and a tint, never a scale or an offset. Two guards follow the repaint: the Overview paints the shared hint model itself rather than using the bordered bar, and the action lane is its own composable.
The fidelity pass stopped at the Overview, so the destinations still carried habits from the sheet. Launch mode's eyebrow and headline both read Launch Mode, and Tune's headline repeated the button the user had just pressed rather than naming the profile it is about to change. The rail and the destination now call the same place the same thing: Tune. Launch mode says what it is for. Tune leads with the profile and separates state you read from actions you take, so a readout like the Steam launch mode no longer sits in the same shape as Reset Game Profile — one is a statement, the other has consequences.
d5f7469 to
4925620
Compare
Pressing Play on a game that is already running is wrong twice over: it relaunches what could have been resumed, and on someone else's session it launches over them. The window can see the session — it holds the api client — so the verb is derived rather than assumed: Watch when another device owns it, Resume when you do, and the launch label otherwise. Yours also gains End session. Resuming and ending both need stream credentials the window does not carry, so it returns the intent and the library performs it, which is the contract the launch already uses. Making that reachable needed a library change. While a session is live the library drops that game from the grid and shows it only in the resume row, whose card resumed along with its button — so the running game was the one entry whose detail could not be opened at all. The card now opens the game and the buttons act on it, which is what the card already did everywhere else. Verified against a live Control session on the Retroid: Resume and End session both appear, and End reaches the host through the result contract and its confirmation.
The verb follows the session —
|
Anchoring the content bottom-left is a landscape assumption. On a phone it stranded everything at the floor of a very tall screen, and it cropped a 3:1 Steam hero into a 0.45:1 viewport, which showed a narrow vertical slice of hair rather than a subject. Portrait now puts the hero in a 16:9 band that dissolves into the window colour, sets the content directly beneath it, stacks the actions at a shared full width so labels stop truncating, and pins the floor to the bottom of the screen instead of trailing the buttons. Verified on a Pixel 10 Pro under Material You, which also covers the theme whose accent is arbitrary because it comes from the wallpaper. The Retroid could not test any of this: its panel is hardware-mounted landscape and ignores rotation. The band leaves empty ground beneath the actions. That is where the playtime and How Long To Beat gauge goes once Polaris can supply it; filler would be worse.
A phone in landscape is barely 430dp tall and the Retroid is 468dp. A header sized for a tall viewport ate a third of that before any content, so Tune showed one and a half cards and clipped Safer Fallback. Below 500dp the eyebrow and headline say the same thing twice over, so they collapse to one line and the room goes to the content they introduce. The insight card had two separate faults meeting in one row. It titles itself from the profile label and then opened its provenance chip with the same word, the same duplication the destination headers had. And that provenance is six facts joined by separators, a metadata line rather than a tag, so a chip could only ever ellipsise it. The line moves under the title where it can wrap, and the reasoning gets the room the scroll already had. Verified on the Retroid against a live host: Tune now shows Steam Launch, Holding and Safer Fallback together, the provenance reads to its end, and the explanation finishes its sentence.
The drill-ins sat on an opaque ground, which read as a separate screen rather than a layer over the game. The side panel is now translucent and leans on the dimmed game beside it for separation. The full-screen studio keeps a solid ground: with no outside to contrast against, translucency only printed the Overview through the artwork controls. Back was the only way out. The dimmed area beside the panel now dismisses on tap, and every destination carries a close control for the cases where there is no outside to tap, which is portrait and the studio. Tune wasted its width. A 500dp landscape body ran one narrow column and pushed the actions below the fold. Splitting it by category put State against Insight, which looked balanced and was not: one short card against two of prose, so the left half sat empty while the right half wrapped every line. The row now carries the two short things, State and Actions, and the prose spans the full width where it reads. The studio opens expanded when it is the whole window, since starting collapsed there costs a tap and leaves the window empty below one row. Verified on the Retroid against a live host, in landscape at 468dp: State and Actions share the row, Reset Game Profile is above the fold, the insight cards read to their end, tapping beside the panel returns to the Overview, and Close returns from the studio.
Dimming with the window colour is only dimming while the window is dark. Portable Chrome is a light theme, so the same expression painted a white veil over the artwork: the hero washed to flat grey and the identity line beside the panel went nearly illegible, because that text is drawn in on-media colours that assume a darkened image underneath. A scrim is a shadow rather than a surface, so it stays dark whatever the theme is. Checked in landscape on the Retroid against a live host across all six themes. Polaris, OLED, Miami, High Contrast and Material You were already correct and are unchanged by this; Portable Chrome now dims rather than bleaches.
…rofile On device the destination said its own name three times: the header, a card titled Launch Mode inside it, and a badge repeating what the mode pills already say. It then spent the rest of the panel on AI Preference, the Launch Profile summary and Reset Game Profile, which are Tune's and which Tune now holds under State and Actions. Keeping them here was the clutter this window was built to remove, moved one screen over. What is left is the question the concept named it for: which of the two modes it runs in, why one is preferred, and the way to fuller settings. The intro stops truncating mid-sentence now that it has the room. Removing that duplicate took the only entry to the profile preference picker with it, leaving the picker with no caller at all. The concept leads Tune's State with the profile itself, above MangoHUD and Steam launch mode, and that is where it goes. Pickers also stop rendering inside the State column, where half a body truncated the title and pushed the options past the fold; they span the body, above the groups. Choosing a mode now returns to the Overview, as the concept specifies. Verified on the Retroid against a live host with a virtual display backend available, so this destination could be reached at all for the first time. Known, pre-existing and not addressed here: selecting a launch mode persists but does not refresh the in-memory state, so the Selected pill still shows the previous mode until the window is reopened.
The only requestFocus in the whole window was the Overview's play button, so opening a destination left focus with no anchor: nothing took it on entry and the d-pad moved nothing at all, on a handheld whose primary input is a d-pad. The body is now a focus group that asks for focus once it has been laid out, which is the idiom the library already uses for its own drawer. Once focus had somewhere to land it could also leave. Pressing left walked out of the panel and onto the Overview's Launch Mode button, dimmed behind the scrim, off the panel and one press from acting. While a destination is open the Overview is scenery, so it stops taking focus for as long as that is true. Verified on the Retroid in landscape: Tune opens with the profile row focused, down moves within a group, right crosses to the next one, and four presses of left now stay inside the panel instead of falling through to the screen behind it.
The drawers read as a stack of rounded bubbles: cards floating inside a panel, each large and mostly empty, with small text inside them. Wide and barren at the same time. The approved concept never asked for cards. It specified 48dp rows in a 438dp lane, label on the left and value on the right, and focus shown as an inset accent bar that does not move the row. So the card goes. Rows run edge to edge, separated by a hairline rather than by air, with the value set in the tabular figures the Overview readout already uses. The two-column split goes with it: that was mine rather than the concept's, and uneven column heights were what left the holes. Tune is one lane again, State then Actions then Insight, and the insight stops being another box. The rest of the drawn tokens follow: the lane is 53% rather than 60%, the headline is 27sp rather than 22, group labels are 8sp, and the scrim is back to the 58% it was specified at. One trap worth recording. Replacing the card cost the rows their focus, because Modifier.clickable alone did not register them as focus targets and the d-pad had nothing to reach — six presses moved nothing and scrolled nothing. Every other focusable in this app pairs clickable with an explicit focusable(), and so does this one now. Verified on the Retroid in landscape: rows take focus on entry, down moves between them, and the focused row shows the bar and tint without moving.
Tune became rows but launch mode still offered its two modes as rounded pills side by side, with its settings as another pill underneath — the same bubbles, one screen over. The concept draws these as a list: the mode, why you would pick it, and where it stands on the right. So they are the same row as everything else now, carrying Selected, Recommended, Available or Unavailable as their value, and the captions say what each mode actually does rather than leaving the label to carry it alone. The choice pill has no callers left and goes. Three guards used it as a section boundary or pinned its 52dp height; they move to the composable that is really there and to the shared 48dp row token, which is what enforces that height now. The heads-up banner stays a banner, as drawn. Verified on the Retroid in landscape: both modes and the settings row read as one list, focus lands on the selected mode and moves between them with the accent bar.
…theme Two versions of one mistake. A fixed alpha is not a fixed strength: the accent is light against a dark surface and dark against a light one, so the focus tint that reads as a whisper under Polaris read as an inverted block under Portable Chrome. Measured on device, the focused row there shifted 39 points per channel against 7 to 22 everywhere else. Scaling the alpha by polarity fixes it, but the polarity has to be read from something that tracks the surface. The first attempt asked colors.window and never fired, because under Portable Chrome the panel takes its lightness from surfaces.panel layered over the window, so the window is the wrong ground to ask. It asks the text colour now, which always contrasts with whatever it sits on. That brings the shift to 29 points, inside the 19 to 33 the other five sit in. The heads-up badge had the same shape of bug from the other direction: it painted its label in the tone colour on a translucent control surface, over a ground already tinted with that same tone. Amber on amber, illegible, and only visible now that launch mode can be opened at all. A badge is a surface rather than media, so it takes an opaque tone fill with ink chosen from that tone's own luminance. Swept all six themes on the rebuilt rows, since the earlier sweep predates them.
The concept pins the header and the hint bar and dissolves the last 52dp of the body, so what is below the fold reads as continuing rather than as clipped. Until now the insight simply collided with the hint bar mid-word. The fade erases content alpha rather than painting a ground over it. The panel is translucent, so a solid band would have striped window colour across the artwork showing through it.
Choosing in the Launch Mode destination did nothing. It wrote into launchMode.preferredMode, and that field means the app's own default, which the resolver deliberately places below the host's configured streamDisplayMode. So the row went on reading Selected against the mode you had just moved away from, and the choice only appeared to stick in the window before client settings had loaded, which is why it looked like staleness and flip-flopped between visits. The hierarchy itself is right and two tests pin it, so this does not reorder anything. The contract keeps being written exactly as before, so Polaris still learns the preference. What is new is a per-game override kept on this client and consulted above the host default, written only by someone actually choosing in that destination. A game nobody has chosen for resolves exactly as it did. Verified on the Retroid: choosing writes launch_mode_override_<uuid>, the row updates without leaving the destination, and it still reads the same after a cold start, which is the case that used to disagree with itself.
Splitting the bottom sheet into an activity and its content moved the fragment, dialog and coroutine machinery out but left their imports where they were, and Android lint does not fail on those. Forty-four of them across the two drawer files, each confirmed unused by the compiler rather than by eye. The two delegation imports that carry no textual reference, getValue and setValue, are deliberately kept: by mutableStateOf needs them and nothing names them.
…rder Two assertions I re-pointed tonight compared indexes with a bare greater-than. That passes vacuously when the left-hand string disappears, since a missing needle returns -1 and any real position beats it — which is exactly how the MangoHUD guard came to be asserting nothing once the composable it referenced was deleted. Both move to the in-0-until form already used elsewhere in this file, which says both things exist and one precedes the other.
The drawer was full width but its rows were not. The panel padded its whole column by the 28dp inset and then again by the safe area, so every divider, focus tint and accent bar stopped about 57dp short of each edge. The list read as a narrow column floating on a wide empty surface, which is the gap on either side. The inset moves off the body and onto the things that want it — header, hint bar, group labels, prose, the notice — so a row spans the panel and carries its own inset for its text. The safe area splits by axis with it: a cutout must not eat text, but it need not stop a row background from reaching the edge it is drawn against, so the panel keeps the vertical inset and everything holding text takes the horizontal one. The focus bar now starts at the edge it is meant to mark, and the hairline separates the whole width rather than a column of it.
Polaris reads minutes from Steam localconfig.vdf and serves them as playtime_minutes. Nova carries it through the model and the adapter, clamped at zero so a host that says something impossible reads the same as one that says nothing: no duration to show. The field sits last in the constructor rather than beside lastLaunched where it reads best. Sixteen places build PolarisGame positionally, and a parameter inserted into the middle shifts every one of them silently — the nvstream contract test caught it by passing a Boolean where a Long had just appeared. Serialisation is by name, so the position costs nothing. No UI yet: the gauge waits until a host is actually serving the field, so it can be built against a real number rather than a zero.
Polaris serves play_time now, read from the launcher that owns the game rather than accumulated from streamed sessions — a game with two hundred hours at the desk would otherwise read as however long it had been streamed to a handheld. Carried as an object rather than a number, and nullable rather than zeroed, because the concept is explicit that absent is not zero: a game nobody has played and a game no launcher can speak for are different answers, and only one of them should ever read "Not started". The concept draws a gauge whose notches are the three How Long To Beat estimates. None exist yet, and it says what to draw in that case — the played figure alone, no bar, since there is nothing to measure against. So that is what this is, and the bar arrives with the data rather than ahead of it. It sits under the hairline that divides names from numbers, because a duration is a number. The field is last in the constructor rather than beside lastLaunched where it reads best. Sixteen places build PolarisGame positionally, and a parameter inserted into the middle shifts every one of them silently; the nvstream contract test caught it by passing a Boolean where a Long had just appeared. Verified end to end on the Retroid against a live host: Phasmophobia reads 199 h, matching the 11983 minutes in this machine's localconfig.vdf.
Polaris serves beat_time now, matched from a dataset on the host rather than fetched from a third party at runtime. With both halves present the Overview draws what the concept drew: played hours at the left, the three estimates at the right, and a bar whose full width is the completionist figure with notches where main and extras fall. The partial cases are the point, and each is the concept's own answer. Played with no estimates keeps the figure alone, because there is nothing to measure it against. Estimates with nothing played read "Not started" over an empty track. Neither draws nothing at all, and the hairline above still separates identity from machine state. Played past the end caps the bar and lets the figure keep counting, since the hours you actually spent are the honest number even when they run off the end of someone else's estimate. Every figure is optional on its own: a catalogue that knows the main story but not the completionist run says so rather than padding the gap with a zero, and the bar falls back to the longest figure it actually has. Verified on the Retroid against a live host: Phasmophobia reads 199 h played against 6 · 20 · 50 h to beat, with the bar capped and both notches at the twelve and forty percent marks the estimates put them at.
The first pass was written from the concept's prose — played hours left, estimates right — which got the arrangement right and everything else wrong. The drawing says more than the sentence did. Each estimate is labelled, because three bare numbers do not say which is which. The played figure is the bright one and the estimates are dimmer, so the row has an order to read it in. Both are uppercase at the tracking the rest of the identity block uses. The bar is rounded and filled with a gradient rather than flat, and the notches are cut through it in the ground colour with a light ring, overhanging top and bottom — which is the difference between a notch in the bar and a mark painted on it, and is visible only in the drawing. The estimate also becomes a control when the dataset carries a page for it: padded, rounded, and in the focus lane with the accent ring. Without a page it is only something to read, and stays out of the lane rather than offering a link to nowhere. Verified on the Retroid against live data: Phasmophobia reads 199 h played against main 20 h, extras 41 h and 100% 138 h, with the bar capped and both notches placed.
The estimate comes from a fuzzy match against a catalogue, and a wrong match looks exactly like a right one: a plausible number, presented as fact. The concept carries matched_name for this reason — so a bad match is visible and correctable, the way an artwork match already is. Shown only when the names genuinely differ. Punctuation and case disagree constantly between a launcher and a catalogue, and "Control Ultimate Edition" against "Control: Ultimate Edition" is the same answer; saying so would bury the cases that matter. A different word or numeral is not the same answer, so "Slay the Spire 2" matched as "Slay the Spire II" says so, and so would the day something matches the wrong game. Verified on the Retroid: Slay the Spire 2 reads its estimate with the line, Control carries the same difference in the dataset and correctly stays quiet.
…y made recommendedMode falls back to preferredMode when neither the host nor the game contract says anything, so with nothing to go on it echoes the preference back. The badge then lands on whichever mode is not selected, and moves when the selection moves. What made it look random is that the host view arrives late. clientSettings starts null and is only filled during a launch preflight, so a freshly opened window resolves against the contract while a window that has already launched something resolves against the host. Same game, two answers, decided by history the reader cannot see. The badge now appears only when something actually recommended: the host said so, or the contract did. An echo of the preference is not a recommendation and no longer claims to be. The resolver is untouched, so the two tests pinning its hierarchy stay honest. Verified on the Retroid: a freshly opened Launch Mode reads Selected and Available, where it previously read Selected and Recommended for no reason it could point to.
Summary
Stacked on #187, which moved the detail into its own window without changing how it looked. This changes how it looks.
The window opened as a single scroll column stacking ten concerns. The landing screen now answers one question — do I want to play this — and everything else has a destination.
Overview goes full-bleed. It reuses
NovaLibraryCinematicBackdrop, so hero-to-poster fallback, the crossfade and the theme scrims come with it rather than being rebuilt. Curated logo artwork becomes the title treatment at 200×64dp — the same asset the sheet crushed into 56%×46% of a 136dp thumbnail inside a card inside a sheet. Below it, one reading order: who the game is, a hairline, what the primary action will do, then what you can do.The launch path loses its modals.
onPrimaryLaunchbranched three ways and only one of them launched:desktopSteamDecision.requiredoptimizationState.reviewRequiredAlertDialog.BuilderNeither needed porting, because the design already had the right home for each. A choice of where to run belongs in the destination named that. The review is a statement about the profile, and the status line is where the profile lives — so it expands, and the alert's three buttons become the action lane. That removes the last raw
AlertDialogin the detail, which the guards already forbade for launch options and profile preference.Artwork takes the whole window.
NovaArtworkStudioopens with aRowof weightedColumns and cannot fold into 60% of a landscape shell. Tune and Launch mode keep the panel.singleTop, because a fast double-press on a poster otherwise stacks two windows.Deliberately absent: the playtime/HLTB gauge, which has no source until Polaris reads it from the launchers. Session-aware verbs were absent for the same reason and are not any more:
activeSessionis plumbed in now, so the primary action follows the session — Resume when one is running, Watch when it is someone else's.Two bugs the device caught that a mockup could not
surfaces.panel, which is sheet glass scaled again by the user's menu opacity — correct over a dimmed sheet, wrong over a hero. The entire Overview read straight through Artwork. Both now lay their glass over an opaque ground. Same class of mistake as the Portable Chrome button in the concept: a surface is not media.BottomStart. One column owns the floor now.Also from device: the status line and the primary button both said Launch 120 FPS, so the most legible row was spending itself on a word the button underneath was about to say; and an
X Launch Modehint was showing for a button that is not bound to anything.Guard tests
Nine failed, and none was a find-and-replace — several took the sheet's shape as their subject: a hint bar inside the scroll column, a footer composed outside it, artwork ordered below the insights, a 136dp hero in an identity panel. That shape is what this change removes. Each assertion is re-pointed at the structure that now carries the same guarantee, and two get stronger:
composeBottomSheetsUseThemeAwareGlassHost…requiredNativeAlertsUseSharedOpacityAndBlurChromedesktopSteamDecisionSheetUsesNovaGlass…LAUNCH_MODE, and a blocked option stays visible with its reasongameDetailLaunchControlsPrioritizePrimaryPlayFocusgameDetailUsesHeroBackdropLogoTransform…The desktop-Steam routing cannot be exercised on hardware without a game running on the host desktop — Polaris's
desktopGameDetectionispolaris_running_app_only. For that path these assertions are the verification, which is what a source guard is for.What followed, on the device
The window landed. The drawers inside it did not, and everything below came from using them on the
Retroid rather than from reading them.
The drawers were the wrong shape. They read as rounded bubbles floating in a padded column —
wide and barren at the same time, small text inside large boxes. The concept never asked for cards.
It specified 48dp rows in a 438dp lane, label left and value right, and focus shown as an inset
accent bar that does not move the row. So the cards go, and so does the two-column split that was
mine rather than the concept's: uneven column heights were what left the holes. The panel padding
went with it. A drawer was full width while its rows were not, every divider and focus tint stopping
about 57dp short of each edge, so the inset moved off the body and onto the things that want it, and
the safe area split by axis — a cutout must not eat text, but it need not stop a row background from
reaching the edge it is drawn against.
Nothing could reach the drawers by d-pad. The only
requestFocusin the window was theOverview's play button, so a destination opened with focus nowhere and the d-pad moved nothing at
all — on a handheld whose primary input is a d-pad. Once focus had somewhere to land it could also
leave: pressing left walked out of the panel onto the Overview's Launch Mode button, dimmed behind
the scrim and one press from acting. The body is a focus group now, and the Overview is scenery
while a destination is open.
Launch mode was answering the wrong question. It said its own name three times — header, a card
titled Launch Mode, and a badge repeating what the mode pills already said — then spent the rest of
the panel on AI Preference, the Launch Profile summary and Reset Game Profile. Those are Tune's, and
Tune holds them now. Removing the duplicate took the only entry to the profile preference picker
with it, so that moves to the head of Tune's State, where the concept puts it.
Choosing a launch mode did nothing. It wrote into
launchMode.preferredMode, which means theapp's own default and sits below the host's
streamDisplayModein resolution — so the row went onreading Selected against the mode you had just moved away from, and appeared to stick only in the
window before client settings loaded. The hierarchy is right and two tests pin it, so this does not
reorder anything: the contract is still written, Polaris still learns the preference, and a per-game
override kept on the client is consulted above the host default. A game nobody has chosen for
resolves exactly as before.
Three the themes caught, and one measurement that lied
The theme sweep ran twice, because the first predates the rows.
dark; under Portable Chrome the same expression painted a white veil, washing the hero to flat grey
and taking the identity line with it. A scrim is a shadow rather than a surface, so it stays dark.
points per channel under Portable Chrome against 7 to 22 everywhere else. The first fix asked
colors.windowand silently never fired, because that theme takes its lightness fromsurfaces.panellayered over the window — the wrong ground to ask. Reading polarity from the textcolour brings it to 29, inside the 19 to 33 the other five sit in. Worth recording that the visual
check could not tell 39 from 29; only measuring the pixels could.
already tinted with that tone. Illegible in light themes, and only visible at all once launch mode
could be opened. A badge is a surface rather than media, so it takes an opaque fill with ink chosen
from that tone's own luminance.
Two traps in the guards themselves
gameDetailKeepsMangoHudOutOfPrimaryLaunchDrawerhad quietly stopped asserting anything: it comparedindexOfagainst a composable that no longer existed, and a missing needle returns -1, which any realposition beats. Two assertions re-pointed during this work had the same shape and were rewritten to
the
in 0 untilform used elsewhere in that file, which says both things exist and one precedes theother.
Separately, 44 imports left behind by #187's split of the sheet into an activity and its content were
removed — each confirmed unused by the compiler rather than by eye, and the two delegation imports
that carry no textual reference deliberately kept.
Exact candidate
8f17f7f28031825d421be630520a0ac75391cfa0571c227b6db3b0a7f4b41b30f5f035381b88c4b1nova/game-detail-window(feat(nova): open game detail in its own window instead of a bottom sheet #187)Verification
Every commit on the branch was taken through the full suite before it was pushed.
:app:testNonRoot_gameDebugUnitTest— 1,170 tests, 0 failures:app:lintNonRoot_gameDebug -PlintFailOnError=true— CI's exact invocation:app:assembleNonRoot_gameDebug/Release/DebugAndroidTestThree instrumented tests fail —
NovaLibraryActivityComposeTest(both shell tests) andNovaPolarisSyncSheetComposeTest. They fail identically when checked out atc43ba29a, the headthis branch had before any of the above, so they are pre-existing rather than caused here. The cause
looks environmental:
connectedAndroidTestreinstalls the app, which wipes pairing, and all threeassert on chrome that only exists once a host is paired —
Search this libraryand the sync sheet'sDesktop Display. Nothing in this branch touches either.On the physical Retroid Pocket 6 against a live host, in landscape:
and four presses of left stay inside the panel instead of falling through to the screen behind
its end, the explanation finishing its sentence
evdion the host: both modes and thesettings row read as one list, choosing returns to the Overview as drawn
library's posters survive the round trip
Not verified on hardware
including the Steam entry. The mechanism is pinned by two guards — the backdrop's
artworkKind = if (hasCachedHero)with both loaders, and the Overview reusing that backdrop — butnothing exercised it for real.
backend at startup and
evdiwas loaded after the running daemon had already looked. The capabilityis advertised, so the destination appears; the daemon needs a restart to use it. That the launch
attempted it at all is the evidence the override reaches the launch.
Known, and not addressed here
land on whichever mode is not selected and change between visits. Unexplained.
Launchbut its action is hard-paired withOPEN_DETAIL, so itopens this window rather than launching. Pre-existing, and library-side rather than this window's.
Resolves the open note on #187
That PR flagged one launch resolving to a different game than the window had open, unexplained. It
reproduced here: launching one game returned a session for another because a resumable session for
the second existed on the host, and the Moonlight protocol resumes an existing session rather than
starting a new app. Host and protocol behaviour, not the result contract.