From 6271bf34c94b97c58ae82c4cfd513dd0d3ef5c18 Mon Sep 17 00:00:00 2001 From: gramps Date: Wed, 21 Oct 2026 20:03:19 -0700 Subject: [PATCH] TASK 60: the border-colour drop-down names every colour The creator: "under Live->Layers->webcam->Properties->border color: in the drop down box, the #hex values are good, but add a corresponding color name/label next to the value." Each row now reads "#e94560 Crimson" - the hex stays exactly as it was, the colour's name goes next to it (dimmed #a0a0b0 so the hex reads first), and the tooltip becomes "Crimson (#e94560)". The 14 colours were a hard-coded string[] inside the click handler - a list of bare strings cannot carry a name, and a list hidden in UI code cannot be asserted on. They now live in Models/BorderColorPalette.cs: NamedColor(Hex, Name) + BorderColorPalette.All (the single source of truth) + NameOf(hex), which answers case-insensitively and falls back to the hex for a colour outside the palette. The picker builds its menu from the catalogue and no longer knows any colour by heart. The names are chosen not to collide - the palette has two greens, two blues and two warm colours: #00FF00 is Lime and #22c55e Emerald, #0000FF is Blue and #3b82f6 Azure, #FF8800 is Orange and #f59e0b Amber. Guard: ytLive.Tests/BorderColorPaletteTests.cs - every entry valid + named, no hex or name repeated, the 14 legacy colours all still offered, NameOf answers, and the old list is gone from the handler. 433/433 green, 0 warnings. --- Controls/LeftPanel.xaml.cs | 28 +++++++++--- Controls/index.md | 2 +- HANDOFF.md | 20 ++++++--- Models/BorderColorPalette.cs | 49 +++++++++++++++++++++ Models/index.md | 3 +- MyMistakes.md | 29 +++++++++++++ TASKS.md | 1 + TASKS/task-60-border-color-names.md | 57 +++++++++++++++++++++++++ ai.md | 5 +++ ytLive.Tests/BorderColorPaletteTests.cs | 53 +++++++++++++++++++++++ 10 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 Models/BorderColorPalette.cs create mode 100644 TASKS/task-60-border-color-names.md create mode 100644 ytLive.Tests/BorderColorPaletteTests.cs diff --git a/Controls/LeftPanel.xaml.cs b/Controls/LeftPanel.xaml.cs index 17096e0..a39b03d 100644 --- a/Controls/LeftPanel.xaml.cs +++ b/Controls/LeftPanel.xaml.cs @@ -247,21 +247,35 @@ public partial class LeftPanel : UserControl if (sender is FrameworkElement fe && fe.ContextMenu == null) { var menu = new System.Windows.Controls.ContextMenu(); - string[] colors = ["#FFFFFF", "#000000", "#FF0000", "#00FF00", "#0000FF", - "#FFFF00", "#FF00FF", "#00FFFF", "#FF8800", "#8800FF", - "#e94560", "#22c55e", "#3b82f6", "#f59e0b"]; - foreach (var c in colors) + foreach (var swatch in BorderColorPalette.All) { - var item = new System.Windows.Controls.MenuItem { Header = c, ToolTip = c }; + // TASK 60: the hex stays, and the colour's NAME goes next to it. + var header = new System.Windows.Controls.StackPanel + { + Orientation = System.Windows.Controls.Orientation.Horizontal + }; + header.Children.Add(new System.Windows.Controls.TextBlock { Text = swatch.Hex }); + header.Children.Add(new System.Windows.Controls.TextBlock + { + Text = swatch.Name, + Foreground = new System.Windows.Media.SolidColorBrush( + System.Windows.Media.Color.FromRgb(0xa0, 0xa0, 0xb0)), + Margin = new System.Windows.Thickness(10, 0, 0, 0) + }); + var item = new System.Windows.Controls.MenuItem + { + Header = header, + ToolTip = $"{swatch.Name} ({swatch.Hex})" + }; var brush = new System.Windows.Media.SolidColorBrush( - (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(c)); + (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(swatch.Hex)); item.Icon = new System.Windows.Controls.Border { Width = 16, Height = 16, Background = brush, CornerRadius = new System.Windows.CornerRadius(2) }; - var color = c; + var color = swatch.Hex; item.Click += (_, _) => src.BorderColor = color; menu.Items.Add(item); } diff --git a/Controls/index.md b/Controls/index.md index 2642e5a..de8b379 100644 --- a/Controls/index.md +++ b/Controls/index.md @@ -12,7 +12,7 @@ See [`schema.md`](../schema.md) for the memory-map conventions. | `TopBar.xaml(.cs)` | Row 0 session surface (2026-09-22 redesign): brand + gear menu (left), one segmented REC|ON-AIR mode switch + world actions (Record: Start Recording · Stream: Go Live + Test) + reality line while running (dot/word/elapsed) + End (center), account zone — Sign In or avatar w/ Change Account/Logout context menu (right). `RefreshAvatar()`, `GearButton_Click` | | `PreviewPane.xaml(.cs)` | Row 1 Col 1 center: the 1920×1080 `OverlayCanvas`/`SelectionOverlay`/`BrandFlashLayer`/`SocialBarElement`, text pull-out drawer, and the live-controls row (TRAX / game / mic meters + sliders + speakers + socials). **TASK 57:** dragging a webcam under the vertical tier routes through `MainViewModel.MoveWebcamInVerticalTier` (the six-spot snap inside the cropped band) instead of free-floating over the master, and a resize re-snaps the tile onto its spot (TASK 57 Unit 2: the resize routes through `MainViewModel.ResizeWebcamInVerticalTier` — the Chat camera's two sizes, then the snap) | | `SceneThumbnailStrip.xaml(.cs)` | Row 0 scene-picker strip (`ItemsControl` over `Scenes`), staged-red / live-green border highlight | -| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur) | +| `LeftPanel.xaml(.cs)` | Row 1 Col 0 left panel: edit mode → layers+properties; live → YouTube chat. Source add/edit/remove, list drag-to-reorder, snapshot revert-accept. **TASK 51:** the webcam properties carry the Background section — `RemoveBackgroundPill` / `BlurBackgroundPill` (`PillToggle`, radio-exclusive via `WebcamSceneConfig.RemoveBackground`/`BlurBackground`) + `BackgroundStrengthSlider` (0..1, 5% snaps, shown only while an effect is on). **TASK 57:** the webcam properties carry a Geometry section (X/Y/W/H) — manual placement of the camera, bound to the **live** placement (the one the current tier shows), gated on `IsWebcamSelected` (not `SelectedElement.IsWebcam` — the TASK 51 test finds that panel by binding text and would match twice); the Shape button's `Visibility` is bound to `CanToggleClipShape`, so the circle option is NOT OFFERED in the vertical Chat layout. **TASK 58:** every drop-down, check box and field in the properties panel carries the theme's style (`YtComboBox`/`YtCheckBox`/`YtTextBoxCompact`) and paints no colour of its own — the guard is `ytLive.Tests/PropertiesStyleTests.cs`. **TASK 59:** the webcam's properties are a HIERARCHY — a `Shape` title owns the mirror switch and the shape choice (`Rect` | `Circle` as two `SegmentToggle` options, Rect selected by default, the circle option hidden where `CanToggleClipShape` is false), and every titled block (Geometry, Border Color, Border Thickness, Background) owns its content one indent deeper (Strength sits under Blur). **TASK 60:** the border-colour drop-down builds its menu from `Models/BorderColorPalette.cs` — each row is the hex plus the colour's NAME (`#e94560 Crimson`, tooltip "Crimson (#e94560)"); the swatch icon is unchanged | | `OverlayHost.xaml(.cs)` | Root-grid overlay dialogs: Settings / Report Bug / Feature Request / About (+ licenses + license-key entry). `BackToAbout_Click` lives here | | `SceneThumbnailStrip.xaml.cs` | Code-behind for the strip above | | `LeftPanel.xaml.cs` | Code-behind for the panel above | diff --git a/HANDOFF.md b/HANDOFF.md index 5fc32e8..977a3a0 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -34,6 +34,12 @@ (the old single button showed the *next* action, "Round"). The mutual exclusion lives in the model (`IsRectShape`/`IsRoundShape` on `SceneElement`) — a WPF `RadioButton` group would kill the bindings. Guard: `ytLive.Tests/PropertiesHierarchyTests.cs`. +- **TASK 60 — the border-colour drop-down names every colour** (shipped 2026-10-21). The creator: + *"the #hex values are good, but add a corresponding color name/label next to the value"* — each row + now reads `#e94560 Crimson` (the name dimmed, the tooltip "Crimson (#e94560)"). The 14 colours + moved out of the click handler into `Models/BorderColorPalette.cs` (`NamedColor(Hex, Name)` + + `NameOf(hex)`), so the list is testable and extensible. Guard: + `ytLive.Tests/BorderColorPaletteTests.cs`. - **The previous session (HA lights) produced NO ytLive code** — it was spent on the creator's Home Assistant light alert system (off-repo, `192.168.50.145`). See `MyMistakes.md` → 2026-10-21: the plan was one tweak to a verified-working script; the script was rewritten twice, the alert path broke, and @@ -169,10 +175,6 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel the full top area and the circle option is not offered. **PASSES.** ## Next step -- **The creator's remaining ask (not started):** the webcam **Border Color** drop-down should show the - **colour NAME next to the hex** (e.g. `#e94560 Crimson`) — the hex stays, the name is the addition. - Plan: a testable hex→name catalogue (Models/Helpers) consumed by the code-behind - `WebcamBorderColorPicker_Click` in `Controls/LeftPanel.xaml.cs`; the tooltip becomes "Name (#hex)". - **Creator check (needs the creator's hands)** — the last piece of the vv feature: switch the resolution dropdown to "Vertical 1080p60", confirm the Live camera tile lands inside the band and the **Circle** option (TASK 59 turned the Shape button into a Rect | Circle choice) turns the tile @@ -183,6 +185,10 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel - Nothing else is queued for llamacasty — the creator queues work. ## Open (operational) +- **Follow-up spotted while doing TASK 60 (not fixed — out of scope for that change):** the border + colour swatches in the drop-down are a plain `Border` with the colour as `Background` and **no + border**, so the black (`#000000`) and dark swatches are nearly invisible against the dark menu + (`#16213e`). A 1px `BorderBrush="#2a3a5e"` on the swatch would fix it. - **GOG Galaxy launcher shows "offline" (2026-10-21) — GOG's own overnight outage, NOT Windows.** GOG put `www.gog.com` behind a maintenance page (503) and their Fastly services for `auth.gog.com` / `api.gog.com` / `remote-config.gog.com` return `500 "Fastly error: unknown domain"`; the page itself @@ -207,9 +213,9 @@ Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbel - TASK 55 disk hygiene (`startup.log` uncapped — 23MB and growing every 5s). ## Tests -- Last full run: **432/432 passed, 0 warnings** (2026-10-21, this session — after TASK 59's - `PropertiesHierarchyTests`; the run before it was 431/431 after TASK 58's - `PropertiesStyleTests`). The one failure seen earlier in the session was +- Last full run: **433/433 passed, 0 warnings** (2026-10-21, this session — after TASK 60's + `BorderColorPaletteTests`; the runs before it were 432/432 after TASK 59 and 431/431 after + TASK 58). The one failure seen earlier in the session was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`, which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression. - Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`, diff --git a/Models/BorderColorPalette.cs b/Models/BorderColorPalette.cs new file mode 100644 index 0000000..abaa489 --- /dev/null +++ b/Models/BorderColorPalette.cs @@ -0,0 +1,49 @@ +namespace ytLive.Models; + +/// A colour the border picker offers: its hex value and the plain-English +/// name shown next to it (TASK 60). +/// The hex value, e.g. #e94560. +/// The name the creator reads, e.g. Crimson. +public readonly record struct NamedColor(string Hex, string Name); + +/// The colours the webcam's border-colour drop-down offers — the SINGLE +/// source of truth for the list and for the names. +/// +/// TASK 60 (the creator, 2026-10-21): *"in the drop down box, the #hex values are +/// good, but add a corresponding color name/label next to the value."* The list used to +/// be a hard-coded array in the click handler, which is why a name could not be added +/// to it without touching the UI code. +/// +public static class BorderColorPalette +{ + /// The offered colours, in menu order. + public static IReadOnlyList All { get; } = new NamedColor[] + { + new("#FFFFFF", "White"), + new("#000000", "Black"), + new("#FF0000", "Red"), + new("#00FF00", "Lime"), + new("#0000FF", "Blue"), + new("#FFFF00", "Yellow"), + new("#FF00FF", "Magenta"), + new("#00FFFF", "Cyan"), + new("#FF8800", "Orange"), + new("#8800FF", "Violet"), + new("#e94560", "Crimson"), + new("#22c55e", "Emerald"), + new("#3b82f6", "Azure"), + new("#f59e0b", "Amber"), + }; + + /// The name of a hex value (case-insensitive). A colour that is not in + /// the palette keeps its hex — there is no name to give it. + public static string NameOf(string hex) + { + if (string.IsNullOrWhiteSpace(hex)) return string.Empty; + var trimmed = hex.Trim(); + foreach (var colour in All) + if (string.Equals(colour.Hex, trimmed, StringComparison.OrdinalIgnoreCase)) + return colour.Name; + return trimmed; + } +} diff --git a/Models/index.md b/Models/index.md index e884ec9..36bd033 100644 --- a/Models/index.md +++ b/Models/index.md @@ -7,12 +7,13 @@ Plain data types. No logic beyond what a property can carry. See |------|---------| | `Scene.cs` | A scene: `Name`, `IsHidden`, `IsChatScene`, `IsEditing`, **`HasBackdrop`** (Live-only policy flag, default off — only the canonical Live scene has one, see `SceneCatalog`), `Elements` collection (images + webcam config), `WebcamConfig` accessor | | `SceneCatalog.cs` | The five canonical scenes (Starting/Live/BRB/Chat/Ending) — the product, by name: `All`, `IsCanonical`, `Is(name, canonical)` (case-insensitive trim), `IsChat`, `HasBackdrop(name)` (true only for Live). Drives the empty-DB seed, the "+" missing-scenes menu, and `MainViewModel.EnforceBackdropPolicy` | -| `Source.cs` | A source: `SourceType` enum (DisplayCapture/WindowCapture/Background/Image/TextOverlay/ChatBox/Countdown/WebSource/MediaSource/**AlertBox (TASK 43 — the celebration zone, one per layout)** — **Webcam removed**), `X`/`Y`/`Width`/`Height`, `Opacity`, `IsEnabled`, `ImageSource` (static) / `VideoImageSource` (live frames), `DisplaySource` (`IsLiveCapture` → video, else static), `ClipShape` enum (Traditional/Round), `IsMirrored` + `MirrorScale`, `MirrorButtonText`/`ShapeButtonText`, asset identity, `IsImageSource` (XAML binds this, never `Type`), **backdrop surface**: `IsBackdrop` (permanent bottom layer, never removable), `CaptureKey` (persisted `monitor:`/`window:`/`picker:`), `IsLiveCapture` (`Type` is Display/WindowCapture) | +| `Source.cs` | A source: `SourceType` enum (DisplayCapture/WindowCapture/Background/Image/TextOverlay/ChatBox/Countdown/WebSource/MediaSource/**AlertBox (TASK 43 — the celebration zone, one per layout)** — **Webcam removed**), `X`/`Y`/`Width`/`Height`, `Opacity`, `IsEnabled`, `ImageSource` (static) / `VideoImageSource` (live frames), `DisplaySource` (`IsLiveCapture` → video, else static), `ClipShape` enum (Traditional/Round), `IsMirrored` + `MirrorScale`, `MirrorButtonText`, asset identity, `IsImageSource` (XAML binds this, never `Type`), **backdrop surface**: `IsBackdrop` (permanent bottom layer, never removable), `CaptureKey` (persisted `monitor:`/`window:`/`picker:`), `IsLiveCapture` (`Type` is Display/WindowCapture) | | `SceneElement.cs` | Base for anything placeable in a scene: shared layout/clip/mirror/border surface, `virtual IsWebcam`/`virtual IsImageSource`/`virtual IsZoomLayer` (TASK 52); `ToggleClipShape` + persisted `RectWidth`/`RectHeight` (pre-Round rect so round→rect restores after reload); **TASK 57:** the geometry setters (`X`/`Y`/`Width`/`Height`) are `virtual` so a derived element can CONSTRAIN every write (the webcam clamps itself into the vertical tier's band) instead of every writer re-implementing the rule; `ClipShape` + `ToggleClipShape()` are `virtual` too, so a derived element can CONSTRAIN the shape (the Chat scene's vertical camera is rect-only); **TASK 59:** `IsRectShape`/`IsRoundShape` — the shape as a CHOICE (the properties panel shows Rect | Circle with Rect selected by default), each a settable bool whose setter flips the shape through `ToggleClipShape()` and always re-raises, so the UI snaps back when a write is refused | | `VerticalCameraLayout.cs` | **TASK 57 (the "vv" feature):** the vertical (9:16) tier's camera rules as pure math — `TileWidth`/`TileHeight` (270×152 master px = 480×270 in the 1080×1920 output), `Inset` (32 master px from the band's outer edge), `IsVerticalTier(QualityOption)` (portrait tier), `AreaFor(band, bottom)` (the band splits into two 607×540 areas), `MaxSizeFor(band)`, `SpotFor(band, w, h, centreX, centreY)` (the six spots — the area and column the tile's **centre** falls in, inset, never outside the band) and `DefaultSpot` (bottom-right, the corner the 16:9 default webcam uses), `FillsArea(band, width)` (the camera is wide enough to fill a whole area = "enlarged") and `SizeFor(band, requestedWidth)` (the Chat camera's TWO sizes - the small tile or one whole 607×540 area — whichever is nearer the requested width, so the resize handle has two detents) | | `ZoomSceneElement.cs` | **Transient zoom box (TASK 52, creator-corrected 2026-10-08):** carries the box rect AND the anchored `CaptureX`/`CaptureY` (the fixed box/2 capture window set on `ActivateZoom`; moving the box never touches it). `IsZoomActive` flips `Kind` Static→Dynamic (leaves the baked base while active); never persisted (LayoutStore save skips non-Source/non-WebcamSceneConfig rows), never shown in the layers list or the element template (`IsZoomLayer` gates both). The compositor SKIPS the active element in every layer loop and draws its 2x content LAST (layer n+1) via `ZoomMagnifier`. | | `Webcam.cs` | Singleton webcam identity: `Id`, `DeviceId`, `Name` (one row app-wide) | | `BackgroundEffect.cs` | **TASK 51:** per-webcam background effect enum `None` / `Remove` / `Blur`, carried by `WebcamSceneConfig` | +| `BorderColorPalette.cs` | **TASK 60:** the colours the webcam's border-colour drop-down offers — `NamedColor(Hex, Name)` + `BorderColorPalette.All` (the single source of truth; the picker builds its menu from it) and `NameOf(hex)` (case-insensitive; a colour outside the palette keeps its hex). The creator asked for a colour NAME next to each hex: `#e94560 Crimson` | | `WebcamSceneConfig.cs` | Per-scene webcam placement (subclass of `SceneElement`): geometry + `IsVisible` + border (`BorderColor`/`BorderOpacity`/`BorderWidth`/`BorderAnimation`) + `VideoImageSource`; `WebcamId` links to `Webcam`; **TASK 51:** `BackgroundEffect` + `BackgroundStrength` (0..1, default 0.5) + the radio-pill surface `RemoveBackground`/`BlurBackground` (one on turns the other off; both off = None) + `HasBackgroundEffect` (shows the strength slider); **TASK 57 (the "vv" feature):** the vertical (9:16) tier's OWN placement per scene — `VerticalX/Y/Width/Height/VerticalClipShape/VerticalRectWidth/VerticalRectHeight` (persisted, NULL = no vertical placement yet) + the transient `VerticalBand`/`VerticalLayoutLive`/`LandscapeX…LandscapeRectHeight` (the parked 16:9 layout) + `BeginVerticalLayout`/`EndVerticalLayout`/`CommitLiveLayoutToVertical` (the tier swap) and the `X`/`Y`/`Width`/`Height` overrides that clamp every write into the band; `VerticalShapeLocked` (the Chat scene's vertical layout is rect-only) **overrides** `ClipShape` + `ToggleClipShape()` so no writer can slip a circle in | | `QualityOption.cs` | Resolution tier record `(Display, Fps, Bitrate, Width, Height)`; `Label` formats as `1080p60 (8Mbps)` — drives the bottom-bar dropdown and the output-rect math | | `StreamConfig.cs` | `StreamConfig` (note: `TargetBitrate`/`Resolution` defaults are stale — not yet wired to the dropdown), `StreamHealth` (bitrate/FPS/dropped/duration), `StreamStatus` enum | diff --git a/MyMistakes.md b/MyMistakes.md index e19eae4..40bed42 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -2142,3 +2142,32 @@ holds the truth. **Bonus:** the same "title owns its content, content is indented one level" rule applies to any dense settings panel — one indent level per ownership level, two when a title is itself nested (Strength under Blur under Background). + +## 2026-10-21 — A LIST THE CREATOR MAY WANT TO EXTEND BELONGS IN A TESTABLE CATALOGUE (RECIPE — TASK 60) + +The creator asked for one thing ("add a colour name next to the hex") and the code could not +even express it: the drop-down's colours were a `string[]` **inside the click handler**. A +list of bare strings cannot carry a name, and a list hidden in UI code cannot be asserted on +without reading the UI code. + +**The recipe (catalogue → UI consumer):** +1. **Move the list out of the handler into a plain data type** — `NamedColor(Hex, Name)` + + `static IReadOnlyList All` in `Models/`. Models = "plain data types, no logic + beyond what a property can carry" (see `Models/index.md`). +2. **Give it a lookup** (`NameOf(hex)`, case-insensitive) that answers for a value the user + typed, not only for the palette's own entries — the fallback returns the value itself, so + the UI never shows an empty name. +3. **The UI becomes a consumer**: the handler iterates the catalogue and builds the rows. The + handler must not know any colour by heart. +4. **Guard it in the test**: every entry valid + named, no hex or name repeated, the colours + that used to be offered are still offered (a name is added, never a colour dropped), the + lookup answers, and **the old list is gone from the handler** (a source-level assertion: + `Assert.False(source.Contains("\"#FFFFFF\""))`). +5. **Name the colours so they do not collide** — the palette had two greens, two blues and two + warm colours, so `#00FF00` is **Lime** and `#22c55e` **Emerald**, `#0000FF` **Blue** and + `#3b82f6` **Azure**, `#FF8800` **Orange** and `#f59e0b` **Amber**. A name that repeats + another's meaning is worse than no name. + +**Why this matters beyond names:** the next request ("add a dark mode", "let me type a +custom colour", "reorder them") is now a change in ONE file instead of a change inside a +click handler. diff --git a/TASKS.md b/TASKS.md index 3d17232..592f25c 100644 --- a/TASKS.md +++ b/TASKS.md @@ -75,6 +75,7 @@ | 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | ✅ **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only — the circle is not offered — the tile or one whole 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) | | 58 | **Properties-panel style consistency (the theme owns the look)** | ✅ Done — 2026-10-21 (the creator's "why is the drop-down not in the same style as the others?" — the drop-downs/fields/check boxes had been hand-painted instead of themed, so WPF drew the light system template; now every one carries a theme style, guarded by a test) | [`TASKS/task-58-properties-style-consistency.md`](TASKS/task-58-properties-style-consistency.md) | | 59 | **The properties panel is a hierarchy (titles own their content)** | ✅ Done — 2026-10-21 (the creator's ruling: a "Shape" title above the Mirror/Rect row, the drag hint indented under it, the same for Geometry / Border Color / Border Thickness, Remove + Blur under Background, Strength under Blur — and the shape became a CHOICE: Rect | Circle with Rect selected by default) | [`TASKS/task-59-properties-hierarchy.md`](TASKS/task-59-properties-hierarchy.md) | +| 60 | **The border-colour drop-down names every colour** | ✅ Done — 2026-10-21 (the creator's "the #hex values are good, but add a corresponding color name/label next to the value" — the 14 colours moved out of the click handler into `Models/BorderColorPalette.cs`, and each row now reads "#e94560 Crimson") | [`TASKS/task-60-border-color-names.md`](TASKS/task-60-border-color-names.md) | --- diff --git a/TASKS/task-60-border-color-names.md b/TASKS/task-60-border-color-names.md new file mode 100644 index 0000000..a990de2 --- /dev/null +++ b/TASKS/task-60-border-color-names.md @@ -0,0 +1,57 @@ +# TASK 60 — The border-colour drop-down names every colour + +**Status:** ✅ Done — shipped 2026-10-21. + +## What it is +The creator (2026-10-21), on Live → Layers → webcam → Properties → **Border Color**: + +> in the drop down box, the #hex values are good, but add a corresponding color name/label +> next to the value + +So each row of the drop-down now reads **`#e94560 Crimson`**: the hex stays exactly as +it was, the colour's name goes next to it (dimmed, so the hex reads first), and the +tooltip becomes **`Crimson (#e94560)`**. + +## The palette is the single source of truth +The 14 colours were a hard-coded `string[]` inside the click handler +(`WebcamBorderColorPicker_Click` in `Controls/LeftPanel.xaml.cs`) — a list that could not +carry a name, and a second place to edit if a colour were added. They now live in +**`Models/BorderColorPalette.cs`**: + +```csharp +public readonly record struct NamedColor(string Hex, string Name); + +public static class BorderColorPalette +{ + public static IReadOnlyList All { get; } = new NamedColor[] + { + new("#FFFFFF", "White"), new("#000000", "Black"), new("#FF0000", "Red"), + new("#00FF00", "Lime"), new("#0000FF", "Blue"), new("#FFFF00", "Yellow"), + new("#FF00FF", "Magenta"), new("#00FFFF", "Cyan"), new("#FF8800", "Orange"), + new("#8800FF", "Violet"), new("#e94560", "Crimson"),new("#22c55e", "Emerald"), + new("#3b82f6", "Azure"), new("#f59e0b", "Amber"), + }; + + public static string NameOf(string hex); // case-insensitive; an unknown hex stays as-is +} +``` + +The names are chosen to be unambiguous next to each other (the palette has two greens, +two blues and two warm colours: `#00FF00` is **Lime** and `#22c55e` **Emerald**; +`#0000FF` is **Blue** and `#3b82f6` **Azure**; `#FF8800` is **Orange** and `#f59e0b` +**Amber**). + +`NameOf` also answers for a hex the creator **typed** into the field — the colours +outside the palette simply keep their hex as the "name". + +## The menu row +The `MenuItem`'s `Header` is a horizontal `StackPanel` (the theme's `MenuItem` template +presents the header through a `ContentPresenter`, so any content works): the hex in the +normal foreground, the name dimmed (`#a0a0b0`) with a 10px gap. The swatch `Icon` is +unchanged. + +## Tests +`ytLive.Tests/BorderColorPaletteTests.cs` — the ONE test for this change: every entry has +a valid hex and a name, no hex or name repeats, the 14 colours the picker used to offer +are all still offered, `NameOf` answers in any case and falls back to the hex, and the +picker builds its menu from the palette (no second list left in the code-behind). diff --git a/ai.md b/ai.md index 0cc7cc2..4647b58 100644 --- a/ai.md +++ b/ai.md @@ -278,6 +278,11 @@ C# / WPF (.NET 8) following MVVM: - **Five-scene catalog (`Models/SceneCatalog.cs`):** the product is exactly Starting/Live/BRB/Chat/Ending — work with less, never more (the escape hatch for "more" is OBS). Scenes are matched **by name** (`SceneCatalog.Is`, case-insensitive trim). Empty DBs seed all five; the scenes-header "+" (`ShowAddScene`/`MissingScenes` on `MainViewModel`) only appears while ≥1 canonical scene is missing and its menu lists only the missing ones, re-adding them by name (`AddSceneCommand`). Renaming a canonical scene makes it missing again; `AddScene` rejects non-canonical names. - Theming: all custom styles live in `Themes/Controls.xaml`, merged in `App.xaml` — never duplicate styles per-window (dialog duplicates were consolidated into this dictionary). **Every control in the app carries a theme style (TASK 58, 2026-10-21):** an unstyled `ComboBox`/`CheckBox` falls back to the **light WPF system template** (grey border, system arrow, white popup list) and sticks out of the dark panels — the properties panel's drop-downs had been hand-painted with a `Background`/`Foreground` instead of a style (the chat Font drop-down was a copy of that), and every check box was default. The rule now: `ComboBox` → `YtComboBox`, `CheckBox` → `YtCheckBox`, the dense properties fields → `YtTextBoxCompact` (`BasedOn YtTextBox`: 11px, padding 4,2) — **no hand-painted `Background`/`BorderBrush` on a themed control**. `ytLive.Tests/PropertiesStyleTests.cs` is the guard: it fails if an unstyled drop-down, check box or properties field comes back - **The properties panel is a HIERARCHY (TASK 59, 2026-10-21):** a **title owns its content, and the content is indented one level (10px) under it** — the creator's ruling: *"the 'drag to move' help text needs to be indented under 'Mirror Rect' to show parentage … same with Geometry, Border Color, Border Thickness … Remove should be indented under Background as should Blur … Strength should be indented under Blur."* So the webcam's block reads `Shape` (the mirror switch + the shape choice) → `Geometry` → `Border Color` → `Border Thickness` → `Background` (Remove, Blur, and Strength one level deeper under Blur), each title SemiBold at 11px `#a0a0b0` like the existing ones. **The shape is a CHOICE, not a toggle button:** `IsRectShape`/`IsRoundShape` on `SceneElement` are two settable bools and the panel shows them as two `SegmentToggle` options (Rect | Circle) with **Rect selected by default** (an element that never picked a shape is a rect) — the old single button showed the *next* action ("Round"), which is why the creator could not tell what the camera was. The mutual exclusion lives in the MODEL (each setter flips through `ToggleClipShape()` and both props are re-raised), **not** in a WPF `RadioButton` group — a RadioButton group unchecks its siblings by setting `IsChecked` in code, which silently kills the binding (stackoverflow 7376373). Each setter always re-raises, so a refused write (the vertical Chat layout is rect-only) makes the UI snap back. Guard: `ytLive.Tests/PropertiesHierarchyTests.cs` +- **One list for the offered colours (TASK 60):** the border-colour drop-down builds its menu from + `Models/BorderColorPalette.cs` (`NamedColor(Hex, Name)` + `NameOf(hex)`), NOT from a list inside + the click handler — the creator asked for a colour NAME next to each hex (`#e94560 Crimson`), and a + hard-coded `string[]` cannot carry one. A palette the creator may want to extend belongs in a + testable catalogue, with the UI as a consumer - Resolution tiers: 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → **Vertical 1080p60@8 (9:16, 1080×1920)**. The composition master frame is **always 1920×1080** — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered **607×1080** window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier. Bottom bar: gear icon far left, stream stats (bitrate/FPS/dropped/duration) centered under preview, resolution dropdown far right. A **tooltip** explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height - Crash diagnosis: `AppLog` writes startup checkpoints to `%APPDATA%\ytLlive\startup.log`; `App.xaml.cs` logs `DispatcherUnhandledException`/`AppDomain.UnhandledException`. When WPF won't run from WSL, this log is how you find the failure (it caught the `MenuItemRole.Separator` XAML crash and the ComboBox SelectionBoxItem bug) diff --git a/ytLive.Tests/BorderColorPaletteTests.cs b/ytLive.Tests/BorderColorPaletteTests.cs new file mode 100644 index 0000000..e50a345 --- /dev/null +++ b/ytLive.Tests/BorderColorPaletteTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using Xunit; +using ytLive.Models; + +namespace ytLive.Tests; + +/// TASK 60 — the creator (2026-10-21): *"under Live->Layers->webcam-> +/// Properties->border color: in the drop down box, the #hex values are good, but add a +/// corresponding color name/label next to the value."* +/// +/// One test: the palette is the single source of truth for the drop-down (the +/// picker builds its menu from it and carries no list of its own), every entry has a +/// valid hex and a name, the names are unique, the colours the picker used to offer are +/// all still offered, and the lookup answers for a hex the creator typed. +public class BorderColorPaletteTests +{ + [Fact] + public void The_border_palette_names_every_colour_the_picker_offers() + { + // 1. Every entry is a valid hex with a name, and no hex or name repeats. + Assert.NotEmpty(BorderColorPalette.All); + foreach (var colour in BorderColorPalette.All) + { + Assert.Matches("^#[0-9a-fA-F]{6}$", colour.Hex); + Assert.False(string.IsNullOrWhiteSpace(colour.Name), $"{colour.Hex} has no name"); + } + Assert.Equal(BorderColorPalette.All.Count, + BorderColorPalette.All.Select(c => c.Hex).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + Assert.Equal(BorderColorPalette.All.Count, + BorderColorPalette.All.Select(c => c.Name).Distinct(StringComparer.OrdinalIgnoreCase).Count()); + + // 2. The colours the picker used to offer are all still offered. + string[] legacy = ["#FFFFFF", "#000000", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", + "#FF00FF", "#00FFFF", "#FF8800", "#8800FF", "#e94560", "#22c55e", + "#3b82f6", "#f59e0b"]; + foreach (var hex in legacy) + Assert.Contains(BorderColorPalette.All, c => c.Hex == hex); + + // 3. The lookup answers for a hex in any case, and keeps an unknown hex as-is. + Assert.Equal("Crimson", BorderColorPalette.NameOf("#e94560")); + Assert.Equal("Crimson", BorderColorPalette.NameOf("#E94560")); + Assert.Equal("White", BorderColorPalette.NameOf(" #ffffff ")); + Assert.Equal("#123456", BorderColorPalette.NameOf("#123456")); + + // 4. The picker builds its menu from the catalogue — there is no second list. + var source = SourceAssert.Text("Controls", "LeftPanel.xaml.cs"); + Assert.True(source.Contains("BorderColorPalette"), + "the border picker does not build its menu from the palette"); + Assert.False(source.Contains("\"#FFFFFF\""), + "the border picker still carries its own list of colours"); + } +}