The board becomes the widget: real sliders, a playhead, and a line to walk - #50
Merged
Conversation
… walk The s&box panel was the panel the old vibe format left behind, and it had drifted into a different product from the web widget it came from. It is now drawn against `<skafinity-player>` as its design, and the parts of that design that are not layout live in `UI/SkafinityBoard.cs` — every user-visible string, the m:ss formatting, the playlist row's state word, the vibe-field grid — with no s&box type in the file, so the web can be pointed at it later without a rewrite. It is deliberately outside `Code/Engine/` for now: a file in that folder is a file in the wasm bundle, and adding one costs an AOT re-stage for code nothing on that side calls yet. **The notched tick strips are gone.** s&box does have a slider — `SliderControl`, with Min/Max/Step and a real thumb — and the claim that it does not was old. Each knob is one, snapped to the level grid the seed encodes; a choice field is a `DropDown` rather than a row of cells; the genre strip is a dropdown whose Random entry is still the way back out of a genre. Theming a control this library did not write needs one trick, since no inline style reaches another component's inner panels: `color` inherits and `currentColor` resolves against it, so the wrapper carries the accent and the scss spends it on the track and the thumb. **The transport is now a transport.** Prev/play/next over a seek bar that scrubs, because a `SoundStream` cannot be rewound and a scrub is therefore a new stream on PCM already in memory. Pause is not a suspend for the same reason: it tears the stream down, keeps how far in it got, and comes back there. `SliderControl` reports every mouse-move and has no let-go event, so the board holds the drag and tells the transport once it settles — the same one-seek-per-gesture the web gets from listening to `change` rather than `input`. **"Now playing" said the wrong song, and the seek bar is why it had to stop.** A `SoundStream` is a FIFO: the crossfade into the next song is written while the current one still has bars to play, and `_curN++` happened at push time, so the board named the next song seconds before anyone could hear it. Songs are now queued with the stream time their first sample lands at and promoted when the clock reaches them, which is also where the playhead's position comes from — the two cannot disagree. **Shuffle, and with it the split between a position and a song index.** Everything keyed by n — the ledgers, the PCM cache, Prev/Next, the playlist — is keyed by timeline POSITION now. With shuffle off a position is the song index and the line is one station, which is what makes Prev able to walk back fifty songs that nothing remembers. With it on, every position past the first is its own station at song 0, derived from the root tag through `SeedCodec.RollTagFor` rather than drawn fresh, so a shuffled line is still a line and still reproduces from one string. The seed's genre and vibe get a switch each, because `tag:n[:genre][:vibe]` gives them a part each and one switch over both could not express half the seeds the engine parses; `StationSeed` joins `CurrentSeed` so the two copy buttons can mean the two different things they mean. **And the save stops lying about its channels.** It passed `channels: 1` for `_curRaw`, which is the interleaved stereo buffer `SoundStream( _sr, MusicGen.Channels )` is fed: every in-game save was stereo PCM under a mono header, i.e. half speed with the channels folded into each other. It now passes `MusicGen.Channels`, and the playlist's per-row export renders a song outside the cache rather than refusing. Clears PLAN row 98. Verified by review, by parsing every touched C# file with Roslyn, and by the node and engine suites (556 checks). The s&box half cannot be compiled here, so the API surface — `SliderControl`, `DropDown`, `TimeSince`, `currentColor` — was read out of the s&box source rather than recalled; that is what caught the comparison of a `TimeSince` against a `double`, which has no operator and would not have built.
Every slider was invisible — the transport's two included — because the sizing
went on a wrapper div and `SliderControl` itself was left with no width or height.
It brings its own chrome from the base stylesheet but nothing about its own box, so
a control nobody sized is a control at zero height: it draws as nothing, and no
error is raised anywhere. gambit and terryball both had this right already, with the
same `.wslider { width; height; pointer-events: all }` on the control, so the shape
here is theirs.
Three things came with it. The class carries the accent inline now, so the
currentColor rules can reach the track and the thumb — and they use child
combinators, because the base sheet styles `.slidercontrol > .inner > .track` and a
flatter selector would silently lose to it. `ShowValueTooltip="false"`, since the
built-in bubble renders as a bare black box with a hardcoded background and the knob
header already prints the value. And the seek bar takes `width: auto` rather than
100%, because the base sheet marks the control flex-shrink: 0 and a full-width one
between two time labels would push the row wider than the board.
Type and width, per the listening pass: one `$fs-*` scale at the top of the sheet so
the next "a bit smaller" is one edit, everything a step down from where it was, and
the board out from 900 to 1180.
The sliders were sized, laid out and draggable — songs scrubbed — and painted nothing, which narrows it to the colours rather than the geometry or the base stylesheet (the TextEntry and DropDown beside them are visibly styled by it). Both of their painted parts were invisible: the track sat at white 0.07, and the fill and thumb were `currentColor`, which is a recent engine feature and paints nothing at all on a build without it. So the slider's insides join the borders as NEUTRAL white alphas. That is the rule this stylesheet already had — the theme owns fills, the sheet owns everything a :hover could fight over — and a control this library did not write belongs on the sheet's side of it for a harder reason than taste: no inline style reaches another component's panels, so the accent was never going to get in there. gambit and terryball both leave the internals alone for the same reason. Two things the screenshot showed on the way past. A literal `#` next to a razor expression was swallowed — "now playing 10", and playlist rows with no number sign — so both labels are built in SkafinityBoard now, where they were always supposed to be. And the volume sat mid-row: `margin-left: auto` put the tinker button at the far end of the row below it but did nothing here, so the label beside it grows instead.
Still nothing, which rules out the colours and points at the geometry. Reading the screenshot again: NOTHING visible on that board comes from a base-addon stylesheet. The seed box's fill and border are this sheet's `.seed-input`, the dropdown's background is an inline BtnStyle, and its chevron is an icon panel built in C#. So there was never evidence the base sheet applies here — and without it SliderControl has no size anywhere inside it. A track with no height paints nothing however it is coloured, which is the whole bug: the board sized the CONTROL, so it laid out and dragged and scrubbed songs, while every part inside it stayed at zero. So the slider is described here in full — inner, track, active fill, thumb — and nothing about it is left to a sheet that may not be there. Where that sheet does apply these rules simply win: each is a class deeper than its `.slidercontrol > …` equivalent and the block is scoped under the panel's own element besides. The parts the control sets inline (the fill's width, the thumb's left) are untouched, and the track keeps a horizontal inset because a thumb centred on 0% needs somewhere to hang.
…s label Five knobs across the board is not much per cell, and the header made it worse by pushing the name and the value to opposite ends of one: hard against the right edge, a value sits closer to the NEXT column's name than to its own, so the drums' tone and the name of their character knob read as a single caption — "40% BUSY". They go together at the left now, and where the column header already names the knob the value simply starts the line instead of floating mid-cell. The rest is width the layout can afford without losing a column: a wider gutter between cells and a little more air between rows.
Three fixes from reading a screenshot have not moved the sliders, so the next step is a fact rather than a fourth guess: `skafinity_ui_dump` walks the board's panel tree and logs each panel's element name, runtime type, classes and screen rect. It settles the one question that cannot be answered from the source here — whether `<SliderControl>` resolved to the component at all. A line reading `slidercontrol <Panel>` with no children means the tag fell back to a plain panel, which can neither draw nor be dragged and raises nothing; `<SliderControl>` with an `.inner > .track` under it means the component is real and the rects say which part collapsed. TEMPORARY, and marked as such. The number sign is settled, though, and it was never markup: `Label.Text` treats text LONGER than one character beginning with `#` as a localisation token, looks the rest up as a phrase, and renders what comes back — so `#24` became `24`, while `▶ #24` was left alone for not starting with one. That is why the playlist's rows lost their hash and the row you were hearing kept it. Nothing here builds "#" plus a number any more: on the transport the hash ends the words before it, and in a row it stands alone in a panel of its own, where the length rule leaves it be. The caret becomes a column of its own while we are there, so every row's number starts at the same x.
The panel dump settled what three rounds of CSS could not. SliderControl's insides are all zero height — the volume one carries only its own `.slidercontrol` class, untouched by anything here, and its track still measures 323x0. A track with no height paints nothing however it is coloured, which is why every attempt at the colours moved nothing. Two walls, both visible in that dump. A STATIC class attribute on a razor component is dropped: `class="ska-slider vol-slider"` never arrived, while the seek bar's — whose value is an expression — did, and replaced the control's own class in the bargain. And a stylesheet does not cross into another component's insides: `.inner`, `.track` and `.thumb` are built by that control's razor, so every rule written for them matched nothing. Neither does an inline style, which cannot reach a panel we did not create. The base stylesheet that dresses those parts in gambit and terryball is not applying here either, and nothing else on this board shows base-addon styling to suggest otherwise. So the board owns its slider: three panels, every value set inline from C#, no cascade to depend on. That is the right shape for a drop-in library in any case — it can assume nothing about the stylesheets of the game it lands in. And it hands back something the stock control could never give: the fill and the thumb are painted with the runtime accent, so a host's colour finally reaches the sliders.
…ne else's" This reverts commit 8fc726f.
The board is not a fair place to ask whether SliderControl draws: it has a
stylesheet, a runtime palette, inline styles and five columns of layout around every
slider, and any of those could be the thing in the way. So this asks the question on
its own — a plain panel, its own ScreenPanel, and three sliders that differ only in
what is on them.
1. bare, no class and no styles at all;
2. `.wslider` — width, height, pointer-events on the control, which is the whole
of what gambit and terryball do and the reason theirs worked first shot;
3. the same inside a box with a visible background, so an invisible slider can be
told apart from a slider that was never built.
All three write to one value, printed at the top, so a drag says which of them is
live even where nothing is drawn. `skafinity_slider_test` puts the card up and takes
it down again; `skafinity_ui_dump_test` prints its rects in the same format as the
board's dump.
Temporary, and the custom slider is reverted while this settles it.
The value moves and nothing draws, which says the control is built and takes input while none of its parts is sized — so the question is no longer about the slider. It is whether a base-addon stylesheet reaches this project at all, and the card can ask that directly. A SwitchControl joins it: another razor component from the base addon, with a sheet of its own that draws something unmistakable — a 48x16 pill with a round knob. If the pill is there, base component stylesheets apply and the slider's trouble is its own. If it is as invisible as the sliders, nothing of the base is reaching this UI, and no markup will draw a control whose every part is sized by a sheet that is not there. A stock <button> goes beside it, whose chrome comes from a plain class rule rather than a component's sheet, to separate "no base stylesheets" from "no base COMPONENT stylesheets". Dropping a sibling panel in would not add to this: test 2 is already gambit's usage verbatim, and both siblings are @inherits PanelComponent with no package references, exactly like this panel. The difference that is left is that they are Type "game" and this is Type "library".
The switch is invisible too, so this is not about sliders: a second base-addon razor component, with its own stylesheet and an unmistakable 48x16 pill to draw, renders nothing here either. (The button probe was badly chosen and says nothing — `.button` in the base sheet only sets position: relative, so it paints nothing even when it does apply. My mistake.) So the panel that is known to work goes in whole. This is terryball's WorldSettings screen with the WIRING removed and nothing else touched — markup, inline styles and the <style> block character for character, minus TerryAvatar (the modal is simply always up), WorldPrefs (fields instead of cookies) and WorldTheme (its colours as literals). `skafinity_slider_test terryball` stands it up in place of the card; the same command with no argument brings the card back, and either way only one is ever on screen. If its sliders draw here, the skafinity board is at fault and that file is the diff to read. If they do not, a panel cannot be the difference, and what is left is the project it is loaded into.
…one else's" This reverts commit 4984761.
terryball's own settings screen, copied here whole and unchanged but for its wiring, does not draw its sliders either. That is the end of the question: a panel known to work where it came from does not work here, so nothing written in this library is the cause, and no arrangement of markup will fix it. The stock control cannot be drawn in this project, and the board keeps SkafinitySlider — three panels of its own, painted inline from C#, which also lets the runtime accent reach the sliders for the first time. Out with the diagnosis: both test cards, `skafinity_slider_test`, the panel-tree dumps. They did their job and they are in the history if it is ever needed again. What stays is the FINDING, in the one place a future session will be standing when it matters — do not reach for a stock Sandbox.UI component in this panel, because it will lay out, accept input, and draw nothing, with no error anywhere.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The s&box panel was the panel the old vibe format left behind, and it had drifted into a different product from the web widget it came from. It is now drawn against
<skafinity-player>as its design, and the parts of that design that are not layout live inUI/SkafinityBoard.cs— every user-visible string, the m:ss formatting, the playlist row's state word, the vibe-field grid — with no s&box type in the file, so the web can be pointed at it later without a rewrite. It is deliberately outsideCode/Engine/for now: a file in that folder is a file in the wasm bundle, and adding one costs an AOT re-stage for code nothing on that side calls yet.The notched tick strips are gone, replaced by real sliders — though not the ones this started with. See the last section.
The transport is now a transport. Prev/play/next over a seek bar that scrubs, because a
SoundStreamcannot be rewound and a scrub is therefore a new stream on PCM already in memory. Pause is not a suspend for the same reason: it tears the stream down, keeps how far in it got, and comes back there. A slider reports every mouse-move and there is no let-go event to wait for, so the board holds the drag and tells the transport once it settles — the same one-seek-per-gesture the web gets from listening tochangerather thaninput."Now playing" said the wrong song, and the seek bar is why it had to stop. A
SoundStreamis a FIFO: the crossfade into the next song is written while the current one still has bars to play, and_curN++happened at push time, so the board named the next song seconds before anyone could hear it. Songs are now queued with the stream time their first sample lands at and promoted when the clock reaches them, which is also where the playhead's position comes from — the two cannot disagree.Shuffle, and with it the split between a position and a song index. Everything keyed by n — the ledgers, the PCM cache, Prev/Next, the playlist — is keyed by timeline POSITION now. With shuffle off a position is the song index and the line is one station, which is what makes Prev able to walk back fifty songs that nothing remembers. With it on, every position past the first is its own station at song 0, derived from the root tag through
SeedCodec.RollTagForrather than drawn fresh, so a shuffled line is still a line and still reproduces from one string. The seed's genre and vibe get a switch each, becausetag:n[:genre][:vibe]gives them a part each and one switch over both could not express half the seeds the engine parses;StationSeedjoinsCurrentSeedso the two copy buttons can mean the two different things they mean.And the save stops lying about its channels. It passed
channels: 1for_curRaw, which is the interleaved stereo bufferSoundStream( _sr, MusicGen.Channels )is fed: every in-game save was stereo PCM under a mono header, i.e. half speed with the channels folded into each other. It now passesMusicGen.Channels, and the playlist's per-row export renders a song outside the cache rather than refusing.Clears PLAN row 98.
The sliders, and why the board builds its own
Worth reading before the next person reaches for a stock control here.
Every slider was invisible. Not mis-styled — absent, while laying out and accepting input: dragging one moved its value. Three rounds of CSS moved nothing, so a console command dumped the live panel tree, and that ended the guessing:
The component is real, its children exist,
.track-activeeven carries the right width for the value — and every height is 0. Nothing sizes those panels: the base addon's stylesheet, which dresses them in a game, does not reach this UI, and a stylesheet here cannot cross into another component's insides to do it instead. Nor can an inline style, which cannot reach a panel we did not create.Two things ruled out a fix in this repo. A stock
SwitchControl— another base component with its own sheet and an unmistakable pill to draw — is equally invisible. And terryball's own settings screen, copied in whole with only its wiring removed, does not draw its sliders here either: a panel known to work where it came from does not work in this project.So
UI/SkafinitySlider.csowns the whole thing — three panels, every value set as an inline style, no cascade depended on. It also hands back something the stock control never could: the fill and thumb are painted with the runtime accent, so a host's colour finally reaches the sliders. The finding is recorded in CLAUDE.md, where a future session will be standing when it matters, and the open question (is this library-vs-game?) is tracked in Gamah/sbox-public#6 rather than left in the code.One smaller engine behaviour came out of the same hunt and is worth knowing:
Label.Texttreats text longer than one character beginning with#as a localisation token, so#24renders as24. That is why playlist rows lost their number sign while the row you were hearing —▶ #24— kept it. Nothing builds "#" plus a number now.Verified by review, by parsing every touched C# file with Roslyn, and by the node and engine suites (556 checks). The s&box half cannot be compiled here, so the API surface —
SliderControl,DropDown,TimeSince,Label,Panel.Style— was read out of the s&box source rather than recalled; that is what caught the comparison of aTimeSinceagainst adouble, which has no operator and would not have built. The panel itself was verified in-game, iteratively, by @Gamah.🤖 Generated with Claude Code