Social bar position is a click-toggle, not a drag — the drag-snap misbehaved for shaky hands (snapped up but wouldn't come back down; jitter around the deadzone). Clicking the bar now flips it top/bottom via ToggleSocialBarPosition; SocialBarSnap and its units removed — 153 tests passing, 0 warnings

This commit is contained in:
2026-08-13 11:04:32 -07:00
parent ac60a26d82
commit ea250c02a6
8 changed files with 58 additions and 141 deletions
+17 -28
View File
@@ -7,30 +7,19 @@
## Session state (last updated: 2026-08-13)
- **Branch:** `main`. TASK 4 **ship step 5.5 is implemented, tested, and staged for
commit** — the social bar bug-fix branch (drag snap + fediverse heal + the bar on
the live output). Uncommitted: the feature work, tests (+8 → 155), and the memory
updates in TASKS.md / ai.md / Services/index.md / this file. Next action is
`git add` + commit + push (feature + its memory docs in ONE commit, per the
working rules).
- **Finished this session:** both reported bugs diagnosed to root cause and fixed;
compositor rendering of the social bar added per user approval.
- **Bug 1 — drag didn't snap:** `Canvas.SetTop` set a local value that permanently
overrides `{Binding SocialBarTop}`; the release-time snap could never win. Fixed
with `SocialBarSnap.Decide` (direction-snap during drag, ±6px deadzone) +
`bar.ClearValue(Canvas.TopProperty)` on release.
- **Bug 2 — Mastodon showed the generic honeycomb:** `@gramps@llamachile.tube`
had `Software = NULL` (nodeinfo only ever asked of the landing-page domain).
Fixed via subdomain probing (`mastodon.``social.` → … candidates, ~15s
budget), a load-time heal (`MainViewModel.HealFediverseSoftwareAsync`), and a
settable `SocialEntry.FediverseSoftware` that raises `LogoData`.
- **Compositor bar:** `Services/Compositor/SocialBarRenderer.cs` (new, WPF
RenderTargetBitmap → straight-alpha strip), `SceneCompositor` blits it above
the flash, `FramePump` gains a re-read-each-frame `socialBar:` seam,
`MainViewModel` owns the strip frame.
- **Verified:** Windows-host build **0 warnings**; **155/155 tests passing**
(147 baseline + 8 new). Full commit contents pending.
- **Branch:** `main`. TASK 4 **ship step 5.5** is **committed and pushed** (`ac60a26`,
"TASK 4 ship step 5.5: social bar bug fixes + bar on the live output").
- **This session (follow-up):** the drag-snap from 5.5 failed in practice — it snapped
up but wouldn't come back down (jitter around the deadzone, per the user). **Superseded
by a click-toggle** (user decision, KISS): `MainWindow.SocialBar_MouseLeftButtonDown`
`MainViewModel.ToggleSocialBarPosition()` flips the bar top ⇄ bottom; the bar rides
`{Binding SocialBarTop}` alone; `SocialBarSnap` + its 2 tests removed. Build 0 warnings,
**153 tests passing** (155 2 snap units). **Uncommitted:** this click-toggle change +
its memory corrections (TASKS.md / ai.md / HANDOFF). Next action: commit + push.
- **Verified:** 0 warnings; 153/153 tests pass.
- **Landmines:**
- Never set a local `Canvas.SetTop` on the social bar — a local value permanently
overrides `{Binding SocialBarTop}` (the `ClearValue` lesson from 5.5).
- The pump reads the active scene on a background thread while the UI can still
edit it — a concurrent-mutation exception is contained (logged + `Failed` +
the pump stops), not a crash. The background thread + video pipeline is the
@@ -49,11 +38,11 @@
integration test (a real `MainWindow`), which never goes live — keep it that way.
- Sandbox can't reach outbound HTTPS — the subdomain-probe logic is verified via
stub-handler tests only, not against the real `mastodon.llamachile.tube`.
- **Next step:** commit + push ship step 5.5 (one commit incl. memory docs). Then
TASK 4 ship step 6 — health stats: bind `FramePump.HealthUpdated`
(bitrate/FPS/duration) into the bottom bar. Nothing else queued — do not expand
the task queue on your own. Optional, not queued: rewriting the healed entry's
`ProfileUrl` to `https://mastodon.llamachile.tube/@gramps` (user must say the word).
- **Next step:** commit + push the click-toggle follow-up. Then TASK 4 ship step 6 —
health stats: bind `FramePump.HealthUpdated` (bitrate/FPS/duration) into the bottom
bar. Nothing else queued — do not expand the task queue on your own. Optional, not
queued: rewriting the healed entry's `ProfileUrl` to
`https://mastodon.llamachile.tube/@gramps` (user must say the word).
- **Secret/DB/port facts live:** OAuth client id/secret in `Helpers/OAuthCredentials.cs`;
OAuth session token in `Helpers/TokenStore.cs` (DPAPI → `%APPDATA%\ytLlive\ytLlive.auth`);
+3 -4
View File
@@ -654,13 +654,12 @@
<!-- Social bar: one centered horizontal row at the top or bottom of
the frame, green glow while enabled. Global resource — never a
Source, no Sources entry. Dragging snaps to the nearest edge. -->
Source, no Sources entry. Click toggles top ⇄ bottom. -->
<Grid x:Name="SocialBarElement" Width="1920" Canvas.Top="{Binding SocialBarTop}"
Background="Transparent"
Visibility="{Binding SocialBarVisible, Converter={StaticResource BoolToVis}}"
PreviewMouseLeftButtonDown="SocialBar_PreviewMouseLeftButtonDown"
PreviewMouseMove="SocialBar_PreviewMouseMove"
PreviewMouseLeftButtonUp="SocialBar_PreviewMouseLeftButtonUp">
MouseLeftButtonDown="SocialBar_MouseLeftButtonDown"
Cursor="Hand">
<Grid.Effect>
<DropShadowEffect Color="{Binding SocialBarGlowBrush.Color}"
BlurRadius="18" ShadowDepth="0" Opacity="0.9"/>
+5 -53
View File
@@ -254,60 +254,12 @@ public partial class MainWindow : Window
private void OpacitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
=> OpacityValueText.Text = $"{Math.Round(e.NewValue * 100)}%";
// ─── Social bar drag: vertical only, direction-snaps to the top/bottom edge ───
private const double SocialBarBottomTop = 1040;
private bool _isDraggingSocialBar;
private double _socialBarDragStartY;
private SocialBarPosition? _socialBarDragDirection;
private void SocialBar_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
// ─── Social bar: click toggles top ⇄ bottom. KISS — the drag variant was
// unusable for shaky hands (jitter around the direction deadzone), so the bar
// rides on {Binding SocialBarTop} alone and a click flips it. ───
private void SocialBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var bar = (FrameworkElement)sender;
_isDraggingSocialBar = true;
_socialBarDragDirection = null;
var toCanvas = CanvasGrid.TransformToVisual(bar).Inverse;
var p = toCanvas.Transform(e.GetPosition(bar));
_socialBarDragStartY = p.Y;
bar.CaptureMouse();
e.Handled = true;
}
private void SocialBar_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (!_isDraggingSocialBar) return;
var bar = (FrameworkElement)sender;
var toCanvas = CanvasGrid.TransformToVisual(bar).Inverse;
var p = toCanvas.Transform(e.GetPosition(bar));
// Derive the drag direction once it commits (past the deadzone) and keep it
// for the rest of the drag — the bar snaps to the edge it's being dragged
// toward and is never left mid-screen.
var direction = _socialBarDragDirection
?? SocialBarSnap.Decide(p.Y - _socialBarDragStartY);
if (direction != null)
{
_socialBarDragDirection = direction;
Canvas.SetTop(bar, direction == SocialBarPosition.Top ? 0 : SocialBarBottomTop);
}
e.Handled = true;
}
private void SocialBar_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (!_isDraggingSocialBar) return;
var bar = (FrameworkElement)sender;
bar.ReleaseMouseCapture();
_isDraggingSocialBar = false;
var direction = _socialBarDragDirection
?? (Canvas.GetTop(bar) <= SocialBarBottomTop / 2.0
? SocialBarPosition.Top : SocialBarPosition.Bottom);
// ClearValue removes the local value Canvas.SetTop applied during the drag —
// a local value permanently overrides the {Binding SocialBarTop}, which is
// why the release snap never used to show. Clearing re-engages the binding.
bar.ClearValue(Canvas.TopProperty);
_viewModel.SetSocialBarPosition(direction);
_viewModel.ToggleSocialBarPosition();
e.Handled = true;
}
-16
View File
@@ -263,19 +263,3 @@ public static class SocialServiceIcons
return true;
}
}
/// <summary>
/// Direction-based snap for the social bar drag (preview): a drag toward the top
/// edge snaps the bar to the top, toward the bottom snaps it to the bottom — the
/// bar is never left mid-screen. Returns null inside the deadzone so a drag that
/// hasn't committed to a direction doesn't flip the bar.
/// </summary>
public static class SocialBarSnap
{
public static SocialBarPosition? Decide(double deltaY, double deadzone = 6)
{
if (deltaY <= -deadzone) return SocialBarPosition.Top;
if (deltaY >= deadzone) return SocialBarPosition.Bottom;
return null;
}
}
+18 -13
View File
@@ -477,12 +477,17 @@ exception is contained (logged + `Failed` + pump stops) rather than crashing.
#### Ship step 5.5 — Social bar bug fixes + the bar on the live output (2026-08-13)
**Bug 1 — drag doesn't snap (root cause found):** `SocialBar_PreviewMouseMove` set `Canvas.SetTop(bar, …)`
with a local value, which permanently overrides the `Canvas.Top="{Binding SocialBarTop}"` binding — the
release-time `SetSocialBarPosition` → `PropertyChanged(SocialBarTop)` could never beat it, so the bar stayed
wherever it was dropped. Fixed by direction-snapping **during** the drag (`SocialBarSnap.Decide` — deadzone
±6px, once the direction commits the bar rides the edge it's dragged toward, never mid-screen) and on release
`bar.ClearValue(Canvas.TopProperty)` re-engages the binding before `SetSocialBarPosition`.
**Bug 1 — bar wouldn't reliably change position (root cause found, then simplified):** the original
drag set `Canvas.SetTop(bar, …)` with a local value, which permanently overrides the
`Canvas.Top="{Binding SocialBarTop}"` binding — the release-time `SetSocialBarPosition` →
`PropertyChanged(SocialBarTop)` could never beat it. First fix added direction-snapping during the drag
(`SocialBarSnap.Decide`, ±6px deadzone) + `bar.ClearValue(Canvas.TopProperty)` on release — but that
still misbehaved for shaky hands (jitter around the deadzone: it snapped up reliably, then refused to
come back down and snapped back to top). **Superseded by a click-toggle (KISS, user decision):** clicking
the bar in the preview flips it top ⇄ bottom (`MainViewModel.ToggleSocialBarPosition` → the existing
`SetSocialBarPosition`), the bar rides `{Binding SocialBarTop}` alone (no local values, no deadzone, no
jitter sensitivity), and `SocialBarSnap` was removed. The `ClearValue` lesson stands: never set a local
value on a property the binding owns.
**Bug 2 — Mastodon showed the generic 7-star honeycomb (root cause found):** the DB row
`@gramps@llamachile.tube` had `Software = NULL` — nodeinfo was only ever resolved against the identity domain
@@ -505,13 +510,13 @@ BGRA strip (1920-wide, 40px content + 24px glow pad, green `#2ecc71` glow baked
`SourceRectHeight bar height`. `MainViewModel` owns the frame (`RenderSocialBarFrame`, re-rendered on
load/save/notify) and feeds the seam.
**Tests (+8 → 155 passing, 0 warnings):** `SocialBarSnap.Decide` units (direction + deadzone + custom
deadzone), settable `FediverseSoftware` updates `LogoData`, subdomain-probe unit + null-when-silent unit,
the branch's **one integration test** `Socials_HealMissingFediverseSoftware_RoundTripsThroughDb`
(temp-DB roundtrip: NULL software → healed via the validator → persisted), compositor bar overlay
(top/bottom + above-flash), `FramePump` bar pass-through (Top then flipped to Bottom mid-run — the seam is
re-read each frame), and both `ISocialValidator` fakes (`FakeValidator`/`BlockingValidator`) gained
`ResolveFediverseSoftwareAsync`.
**Tests (+6 → 153 passing, 0 warnings):** settable `FediverseSoftware` updates `LogoData`,
subdomain-probe unit + null-when-silent unit, the branch's **one integration test**
`Socials_HealMissingFediverseSoftware_RoundTripsThroughDb` (temp-DB roundtrip: NULL software → healed via
the validator → persisted), compositor bar overlay (top/bottom + above-flash), `FramePump` bar pass-through
(Top then flipped to Bottom mid-run — the seam is re-read each frame), and both `ISocialValidator` fakes
(`FakeValidator`/`BlockingValidator`) gained `ResolveFediverseSoftwareAsync`. The drag-snap units
(`SocialBarSnap`) were removed with the click-toggle supersession.
**Not included (say the word):** rewriting the healed entry's `ProfileUrl` to `https://mastodon.llamachile.tube/@gramps`.
+7 -1
View File
@@ -2013,7 +2013,7 @@ public class MainViewModel : ViewModelBase
ScheduleSave();
}
/// <summary>Drag release snaps the bar to the closest edge; called by the preview drag.</summary>
/// <summary>Sets the bar's edge; called by the preview click-toggle.</summary>
public void SetSocialBarPosition(SocialBarPosition position)
{
if (_socials == null || _socials.BarPosition == position) return;
@@ -2022,6 +2022,12 @@ public class MainViewModel : ViewModelBase
ScheduleSave();
}
/// <summary>Preview click flips the bar top ⇄ bottom (KISS — no drag math).</summary>
public void ToggleSocialBarPosition()
=> SetSocialBarPosition(_socials?.BarPosition == SocialBarPosition.Top
? SocialBarPosition.Bottom
: SocialBarPosition.Top);
/// <summary>Signs out of YouTube (the delete-the-YouTube-slot action in the dialog).</summary>
private async Task SignOutYouTubeAsync()
{
+8 -7
View File
@@ -422,8 +422,8 @@ seam:** `Func<Scene?>`, `Func<SceneElement, VideoFrame?>` resolver, `Func<Compos
### Social bar (TASK 14 — shipped 2026-08-12, plan in TASKS.md; bar bug-fix branch 2026-08-13)
A **global bar layer** (never a Source, no sources-list row) that sits over the bottom or top of the
output and carries the creator's social links — content-sized, centered, GREEN glow when ON, top/bottom
snap-drag (default BOTTOM, persisted `SocialBarPosition`). `Models/Socials.cs`: `SocialService` enum
output and carries the creator's social links — content-sized, centered, GREEN glow when ON, position is a
**click-toggle top ⇄ bottom** (default BOTTOM, persisted `SocialBarPosition`). `Models/Socials.cs`: `SocialService` enum
(YouTube/Twitch/X/Instagram/TikTok/Facebook/Discord/Kick/Threads/Bluesky/GitHub/LinkedIn/Pinterest/
Snapchat/Reddit/WhatsApp/Telegram/Link/Website/**Fediverse**), `SocialEntry` (Service/Handle/ProfileUrl/
`FediverseSoftware`), `SocialsConfig` (Entries + `BarPosition` + `BarEnabled`; `BarJustify` dropped,
@@ -447,11 +447,12 @@ never touch slot state).
**Bar bug-fix branch (2026-08-13) — three changes:**
1. **Drag now direction-snaps** — a local `Canvas.SetTop` value permanently overrides the
`{Binding SocialBarTop}` (a binding can never win over a local value), so the old drag left the bar
wherever it was dropped. Now `SocialBarSnap.Decide` (pure, in `Models/Socials.cs`) commits a direction
once the drag passes the ±6px deadzone and rides the edge it's dragged toward; on release
`bar.ClearValue(Canvas.TopProperty)` re-engages the binding before `SetSocialBarPosition`.
1. **Positioning is a click-toggle (KISS)** — the original drag set a local `Canvas.SetTop` value that
permanently overrides the `{Binding SocialBarTop}` (a binding can never win over a local value), so the
bar stayed wherever it was dropped. The first drag-snap fix (`SocialBarSnap.Decide` + `ClearValue` on
release) still failed for shaky hands — jitter around the ±6px deadzone snapped the bar up but wouldn't
let it come back down. **Superseded by the user's click-toggle:** clicking the bar flips it top ⇄ bottom
(`MainViewModel.ToggleSocialBarPosition`), the bar rides the binding alone, and `SocialBarSnap` is gone.
2. **Fediverse software self-heal** — the DB row `@gramps@llamachile.tube` had `Software = NULL` because
nodeinfo was only ever asked of the identity domain (a landing page; the real instance is
`mastodon.llamachile.tube`). `HttpSocialValidator.ResolveFediverseSoftwareAsync` now **probes
-19
View File
@@ -30,25 +30,6 @@ public class SocialBarTests
=> throw new System.NotSupportedException();
}
[Fact]
public void SocialBarSnap_Decide_DirectionAndDeadzone()
{
Assert.Equal(SocialBarPosition.Top, SocialBarSnap.Decide(-10));
Assert.Equal(SocialBarPosition.Top, SocialBarSnap.Decide(-6));
Assert.Equal(SocialBarPosition.Bottom, SocialBarSnap.Decide(10));
Assert.Equal(SocialBarPosition.Bottom, SocialBarSnap.Decide(6));
Assert.Null(SocialBarSnap.Decide(5));
Assert.Null(SocialBarSnap.Decide(0));
Assert.Null(SocialBarSnap.Decide(-5));
}
[Fact]
public void SocialBarSnap_Decide_HonorsCustomDeadzone()
{
Assert.Null(SocialBarSnap.Decide(10, 20));
Assert.Equal(SocialBarPosition.Top, SocialBarSnap.Decide(-21, 20));
}
[Fact]
public void SocialEntry_FediverseSoftware_SettableUpdatesLogo()
{