Skip to content

fix(iced): pin the notification dots to the Mac accent #007aff (#311) - #316

Merged
charliek merged 3 commits into
mainfrom
feature/plan-023-badge
Aug 9, 2026
Merged

fix(iced): pin the notification dots to the Mac accent #007aff (#311)#316
charliek merged 3 commits into
mainfrom
feature/plan-023-badge

Conversation

@charliek

@charliek charliek commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes #311.

Plan 023, workstream A — the last objective item on M4's "no open P0/P1"
entry criterion. Part 1 of 3 (pre-release cleanup); siblings to follow for the
release-smoke extraction and the stale GTK documentation.

What changed

The constant. iced rendered its notification dots #4e9af1 while both
reference implementations deliberately use #007aff — the Mac dots are
NSColor.controlAccentColor and GTK hardcodes the same value rather than
tracking the desktop accent, because @accent_bg_color renders teal on
COSMIC. A dedicated chrome::NOTIFICATION_BADGE now carries the corrected
value, used only by chrome::badge().

NOTIFICATION keeps #4e9af1 and its three other uses — dragged-pill
border, inline-rename focus ring, inline-rename selection. Those are
user-directed polish with no cited divergence, so recoloring them here would
be scope creep into the fenced-off 3h cluster. Whether the generic accent
should follow is filed separately for the owner's call.

The plan's own review caught that this is two surfaces, not one.
chrome::badge() styles both the tab-pill badge (app.rs:1978-1986) and the
sidebar project-row notification dot (app.rs:1777-1786). The 2026-08-07
audit that filed #311 recorded only the tab badge. Both were corrected, and
that is correct rather than incidental: both have a Mac counterpart at
controlAccentColor (App.swift:4772, :5207), so both move toward
parity. Splitting badge() into two styles was considered and rejected — it
would invent a second constant with no reference divergence to justify it.

The test. tab.list exposes only the has_notification bit; the colour
that bit is painted in, and where, is invisible to every dump op. That is why
#311 shipped undetected, and why the parity inventory named a badge
colour/position pixel assertion as its missing acceptance evidence.

test_notification_dots_paint_the_accent asserts, off one app.screenshot
with a single notified inactive tab:

  • exactly one accent blob in the tab strip and one in the sidebar
  • each blob's centre pixel exactly #007aff (found fuzzily at
    COLOR_TOL, asserted exactly)
  • size within the band an 8px dot may occupy
  • the tab badge vertically centred in the 32px band
  • the tab badge bracketed between its own title's last glyph and the next
    pill's fill, with the gap bounded on both sides — relational rather than a
    pinned x, because pill width is text-dependent
  • the sidebar dot at its derived PROJECT_PILL_INSET_X + PROJECT_DOT_INSET
    trailing reserve, so it is provably the project row's dot
  • a clean baseline before the notification, and a clean frame after
    tab.clear_notification

The negative case uses clear_notification, not focus: the render guard is
has_notification && !active, so focusing flips both inputs at once and a
blank result would not say which one did it.

Rust assertions compare literals, not constants against themselves. A test
asserting badge() renders NOTIFICATION_BADGE passes for any value of that
constant. The literal assertions are what pin the value; the wiring assertion
is kept and labelled as such. The two inline-rename uses gain their first
coverage.

Verification

  • make check-iced (with and without linux-package), make test-rust,
    make e2e-iced-ci85 passed, 3 skipped (all three skips pre-existing
    and environmental).
  • The pixel test was mutation-verified twice, not once. Reverting the
    constant to #4e9af1 makes the blob unfindable and fails the count path;
    #0079ff — one off, deliberately inside COLOR_TOL — is still found
    and fails the exact-centre assertion. Only the second mutation proves the
    tolerance is not swallowing the bug.
  • Blob geometry was measured, not guessed: 8×8 in both regions, and the
    badge-to-pill gap measured exactly the 9px the layout constants predict.
    The size band's floor is the same 5px saturated core the repo already pins
    for an 8px dot, since this module runs on four lanes and only one renderer
    was available locally.

Review findings

Three panel reviewers on the plan, plus a codex correctness pass per commit.

Finding Disposition
codex (plan): chrome::badge() has two call sites — the plan claimed one Fixed. Both surfaces corrected, with evidence for both; inventory and roadmap now record what the audit missed
CodeRabbit + codex (plan): proposed colour assertions were tautological Fixed. Literal-value assertions added
GLM + CodeRabbit (plan): the negative case was confounded by focus() Fixed. clear_notification instead, plus a pre-notify baseline
CodeRabbit (plan): the [6,9] size band was an unmeasured guess on a full circle Fixed. Measured first; floor widened to the repo's documented 5px core
codex (C1): the badge()-renders-NOTIFICATION_BADGE assertion is tautological Kept deliberately — it guards the wiring, the literals guard the value — and labelled in a comment
codex (C2): badge-to-pill gap had only an upper bound, so a right-drifted badge would pass Fixed. Bounded both sides
codex (C2): the sidebar blob was only proven to be "in the sidebar" Fixed. Pinned to the project pill's trailing inset

Impact

No dependency, privacy, or secret impact. No wiring needed — this module is
already enumerated in ICED_E2E_TESTS and all three ci.yml lists. It is
deliberately not in ICED_RELEASE_E2E_TESTS: chrome colour is not
profile-dependent.

Plan 023 §D1 — the pinned design (excerpt)

D1.1 — one dedicated constant, shared by both call sites. The first draft
asserted only the tab badge changed. It was wrong: badge() is shared. The
decision stands anyway, now on evidence rather than by accident — both iced
surfaces have a Mac counterpart at controlAccentColor, so recoloring both
moves both toward parity. Splitting badge() into two styles was considered
and rejected: it would create a second constant with no reference divergence
to justify it, and would leave the sidebar dot at a value no implementation
uses.

Alternative considered: flip NOTIFICATION wholesale. Real argument — the
Mac uses controlAccentColor for its drag indicator too
(DragReorder.swift:140) — but it recolors three user-visible surfaces
nobody asked to change, inside the fenced-off 3h cluster. → filed as an issue.

D1.2 — Rust unit assertions: literals, not restatements. The first draft
proposed badge(&Theme::Dark).background == Some(Background::Color( NOTIFICATION_BADGE)), which passes for any value of the constant. The same
flaw is already latent in the existing chrome.rs test.

D1.3 — the pixel test. Fixture with every precondition explicit; a
generalized blob finder (neither existing finder supported a min-x or any y
bound); captures polled rather than taken once; size deliberately not pinned
until measured, because the badge is a full circle where the sidebar's prior
art deliberately uses radius 3 "to retain a solid renderer-neutral edge".

§8 — two mutations, not one. One mutation is not enough: reverting the
constant makes the blob unfindable, so the test fails on the count
assertion and the exact-centre check — the thing that distinguishes this from
"some blue dot exists" — is never exercised.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d

Summary by CodeRabbit

  • Bug Fixes

    • Corrected notification badge coloring to use the standard blue accent across tabs and sidebar project indicators.
    • Preserved the existing notification color for inline rename styling.
  • Tests

    • Added pixel-level checks for badge color, size, placement, spacing, and clearing behavior.
  • Documentation

    • Updated migration and parity records to reflect the completed notification badge fix.

charliek and others added 3 commits August 9, 2026 12:54
Iced rendered both notification dots — the tab-pill badge and the sidebar
project-row dot, which share `chrome::badge()` — at `#4e9af1`, while both
reference implementations deliberately use `#007aff`: the Mac dots are
`NSColor.controlAccentColor` (`App.swift:4772`, `:5207`) and GTK hardcodes
the same value for its tab badge rather than tracking the desktop accent,
because `@accent_bg_color` renders teal on COSMIC.

A dedicated `NOTIFICATION_BADGE` constant carries the corrected value, so
the generic `NOTIFICATION` accent keeps `#4e9af1` and its three other uses
(dragged-pill border, inline-rename focus ring, inline-rename selection)
untouched — those are user-directed polish, not a cited divergence.

The unit assertions compare color *literals*, not the constants against
themselves: a test asserting `badge()` renders `NOTIFICATION_BADGE` passes
for any value of that constant and would not catch a re-flip. The two
inline-rename uses gain their first coverage.

codex review finding: the `badge()`-renders-`NOTIFICATION_BADGE` assertion
is tautological. Kept deliberately — it guards the *wiring* (badge being
repointed at another constant) while the literals guard the value — and
labeled as such in a comment so it isn't mistaken for the color check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d
C1 corrected the constant; nothing proved it reaches the screen. `tab.list`
exposes only the `has_notification` bit, so the colour the bit is painted in
— and where — is invisible to every dump op. That is why #311 shipped
undetected and why the parity inventory names a badge colour/position pixel
assertion as the missing acceptance evidence.

`test_notification_dots_paint_the_accent` covers both surfaces
`chrome::badge()` styles: the tab-pill badge and the sidebar project-row
dot. With one notified INACTIVE tab it asserts exactly one accent blob per
region, each exactly #007aff at its centre (found fuzzily, asserted
exactly), sized near NOTIFICATION_DOT_SIZE, band-centred, the tab badge
bracketed between its own title's last glyph and the next pill's fill, and
the sidebar dot at its derived PROJECT_PILL_INSET_X + PROJECT_DOT_INSET
trailing reserve. A clean baseline before the notification and a clean
frame after `tab.clear_notification` bound it on both sides —
`clear_notification` rather than `focus` because the render guard reads
`has_notification && !active`, so focusing would flip both inputs at once.

Verified non-vacuous by two mutations, not one: reverting the constant to
#4e9af1 makes the blob unfindable and fails the count path, while #0079ff
— one off, deliberately inside COLOR_TOL — is still found and fails the
exact-centre assertion, proving the tolerance is not swallowing the bug.
Transcripts in the plan's artifact folder.

`_blobs()` generalises the two ad-hoc finders in test_sidebar_pixels.py
(neither supported a min-x or any y bound); both are refactored onto it
rather than adding a third near-duplicate scan. Behaviour-equivalent: the
old first-match/break is indistinguishable from independent per-colour
passes because the lifecycle tolerance regions do not overlap.

codex review findings, both fixed: the badge-to-pill gap had only an upper
bound, so a badge drifted right toward the next pill would pass — now
bounded on both sides; and the sidebar blob was only proven to be "in the
sidebar" — now pinned to the project pill's trailing inset.

No test-list wiring needed: this module is already enumerated in
ICED_E2E_TESTS and all three ci.yml lists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d
The parity inventory's Tab close/badge row and the roadmap's open-P1 audit
list both carried the badge divergence as an open finding. Both now record
the fix, and both record what the 2026-08-07 audit missed: `chrome::badge()`
styles two surfaces, not one — the tab-pill badge and the sidebar
project-row dot — and both have a Mac reference at
`NSColor.controlAccentColor`, so both were corrected.

The inventory row moves to the table's existing `P1 → 3h` marking, matching
the sibling rows where the objective half is done and only the
Charlie-directed item (here hover-close) remains. The roadmap's M4
paragraph now reads (c) and (d) as done, leaving the 3h items and the
#302-blocked remainder. M4 is deliberately NOT declared entered, and the
waiver-semantics sentence is untouched: a per-row waiver stays an owner
decision recorded on the inventory row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgxiEBQTqgNjPswKqcs12d
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 66d61506-84fe-417e-81b5-8302f331dbe9

📥 Commits

Reviewing files that changed from the base of the PR and between bc70fc6 and ac6c114.

📒 Files selected for processing (5)
  • crates/roost-iced/src/chrome.rs
  • docs/development/iced-migration-roadmap.md
  • docs/development/iced-parity-inventory.md
  • tools/roosttest/test_sidebar_pixels.py
  • tools/roosttest/test_tab_strip_pixels.py

📝 Walkthrough

Walkthrough

Iced notification badges now use the dedicated #007aff color. Unit tests and screenshot tests cover tab and sidebar badges, including color, size, position, baseline, and clearing. Parity documentation records issue #311 as fixed.

Changes

Notification badge correction

Layer / File(s) Summary
Badge color contract and rendering
crates/roost-iced/src/chrome.rs
Adds NOTIFICATION_BADGE with #007aff. badge() uses it, while inline rename styling keeps NOTIFICATION. Unit tests verify both behaviors.
Badge pixel validation
tools/roosttest/test_sidebar_pixels.py, tools/roosttest/test_tab_strip_pixels.py
Adds reusable blob detection and screenshot assertions for tab and sidebar badges. Tests verify color, dimensions, placement, baseline, and removal after clearing.
Parity audit records
docs/development/iced-migration-roadmap.md, docs/development/iced-parity-inventory.md
Records the badge discrepancy and issue #311 as resolved. M4 criteria now include the completed badge fix.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • charliek/roost#236: Introduced GTK tab-pill badges with related styling and behavior.
  • charliek/roost#305: Added related Iced notification badge behavior and pixel-test coverage.
  • charliek/roost#312: Updated parity roadmap and inventory records related to the badge fix.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the iced notification-dot color fix and references issue #311.
Linked Issues check ✅ Passed The PR satisfies [#311] by wiring #007aff through NOTIFICATION_BADGE and adding pixel coverage for color and geometry.
Out of Scope Changes check ✅ Passed The code, pixel tests, and documentation changes directly support the badge-color correction and its acceptance coverage.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/plan-023-badge

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Iced tab notification badge is #4e9af1; GTK/Mac deliberately use #007aff

1 participant