Skip to content

feat(picker): サムネイルグリッドのエフェクトピッカー — Browse Effects… から開くウィンドウ (#12 の UI 側) - #65

Merged
GeneralD merged 12 commits into
mainfrom
feat/effect-picker
Oct 30, 2026
Merged

GeneralD merged 12 commits into
mainfrom
feat/effect-picker

Conversation

@GeneralD

@GeneralD GeneralD commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

agent type scope part of

概要

issue #12 の UI 側の半分。Snap Camera 風のサムネイルグリッドのエフェクトピッカーを足した。メニューバーの Browse Effects… でウィンドウが開き、検索欄の下にエフェクトがセルで並ぶ。選んでいるセルはメニューのチェックの行と常に同じ。サムネイルは今はプレースホルダ (頭文字と色) で、本物の画像はデータ側のセッションが入れる EffectThumbnail を統合 PR で差し込む。

effect picker

make test のプロセスの中で NSHostingView をオフスクリーンで cacheDisplay した画像 (fixture の 6 件、Ripple を選択、Broken は manifest が無いパッケージ)。実機の swift run dev-harness では撮っていない。

変更点

  • EffectPickerPresenter と EffectPickerGeometry (3f321d6): セル (EffectPickerCell: 名前、2 行目、選択中、選べるか、hover の文、artwork) を、メニューと同じ EffectInteractor.state から作る。2 行目と status 行はメニューの MenuBarText をそのまま使い、同じことを別の言い方で書かない。検索は前後の空白を除いて、名前か作者に含まれるかを大文字小文字・アクセント・全角半角を無視して見る (locale なし)。何も当たらないと No effects match “…”。Presenter は 観察するだけ で、Interactor の start / stop はしない (メニューの Presenter が持ち主で、2 回目の start() は catalog の再起動になる)。グリッドの列数とセル幅は EffectPickerGeometry(width:) の純関数で、Views が coverage の ignore 対象なので Presenters に置いた。例のテストと seed 64 個の property test で固定
  • ビュー (5313d62): EffectPickerView は検索欄の下の LazyVGrid。選択中のセルはアクセント色の枠、壊れたパッケージは薄く押せないセル、hover で名前と作者 (または選べない理由)
  • Router (594db20、窓の移動は ce9f538): メニューの Browse Effects… → MenuBarPresenter.browseEffects() → MenuBarRouting (weak) → AppRouter。Router は注入できる makePickerWindow でウィンドウを作り (live は EffectPickerWindow: EffectPickerView を NSWindow に載せ、閉じたことを自分自身付きで報告する AppKit glue。宣言だけを置く coverage 対象外の Views ではなく AppRouter に置いた)、開いているのは常に 1 枚 (2 回目は同じウィンドウを前に出す)。開いている間だけピッカーの Presenter を start し、タイトルバーで閉じても stop() で閉じても Presenter を止める。既に手放したウィンドウからの遅れた close 報告は何もしない。ホストはピッカーを閉じても終了しない (applicationShouldTerminateAfterLastWindowClosed → false)。ハーネスはプレビューのウィンドウを閉じたら、ピッカーが開いていても終了する (以前の「最後のウィンドウで終了」では、ピッカーが残ると終わらなくなるため)。ハーネスの Effects メニューはホストの EffectMenu なので、同じ項目が出る
  • ドキュメント (75a5c36): docs/ARCHITECTURE.md に Key Design Decision Effect picker を足し、依存グラフに AppRouter --> Views、Layer Summary (Entry / Router / View / Presenter / Entity)、Physical Abstraction Table の Menu bar / windows、Menu bar の決定を直した。AGENTS.md、import allow-list (AppRouter が Views を import する)、View ファイルの idiom (Geometry は Presenters)、Lane 1 の Effects メニューの行と新しい Effect picker の確認行と Quit の行も合わせた
  • レビューでの修正 (5e407ee、ce9f538、bbf403a): 下の「レビュー」を参照
  • version: 0.23.0 (c84c58b)。main が fix: #64 のマージ後レビューの指摘を直し、0.21.1 に bump する (formatVersion の README・テスト行、HarnessMenu.live() の owner guard ほか) #67 (0.21.1) と feat: carry the manifest thumbnail through to the effect catalog (#12, data half) #66 (0.22.0) で進んだので、そのたびに main を取り込んで数え直した
  • 前提の refactor (fa61f42): メニューの Presenter の state の観察 (sink → newest-wins の AsyncStream → main actor の task) を CatalogStateObservation に切り出し、2 つの Presenter で共有した

設計判断

  • ピッカーの Presenter は Interactor を start しない: 開くたびに catalog が再起動するのを避ける。state は最新の値を replay するので、後から開いたピッカーもすぐ一覧を出す。Alternative not taken: ピッカーがメニューの Presenter を読む (Presenter が Presenter に依存し、メニュー無しでピッカーを出せない)
  • サムネイルの差し込み口: セルの artwork は EffectArtwork enum で、今のケースは .placeholder(EffectPlaceholder) だけ。プレースホルダは名前の最初の 2 語の頭文字と、<origin>:<key> の FNV-1a 64 から作る色相 (hashValue と違い、起動ごとに色が変わらない)。統合 PR では EffectCatalogEntry.thumbnail があるときに EffectArtwork に画像のケースを足し、Presenter (EffectPickerText.swift の pickerCell) がどちらを使うか決め、EffectArtworkView がそのケースを描く。他は変わらない
  • ウィンドウは Router が持ち、AppRouter が Views を import する: lyra の wireframe と同じ。Router は PickerWindow protocol 越しにしか見ないので、テストは fake のウィンドウで回る

入れていないもの

  • ライブプレビュー: ホストアプリにはプレビューの経路がまだ無い (CameraInteractor に attach が無く、PreviewView を attach しているのはハーネスの HarnessInteractor だけ)。ピッカーにプレビューを出すには、まずホストにその経路が要る
  • manifest の description の hover 表示: EffectCatalogEntry が description を持っていない。足すとデータ側のセッションと EffectCatalogEntry で衝突するので、hover は名前・作者・選べない理由にした
  • README (Roadmap を含む) と Closes #12 は統合 PR に任せる

レビュー

独立したレビュアー 5 系統 (ライフサイクル、アーキテクチャ規約、テスト、コメント・docs の正確さ、型設計) に diff を見せ、出た指摘 18 件をそれぞれ別の検証役が反証を試みた。残った 10 件はすべて直した。

  • EffectPickerWindow を Views から AppRouter に移した (ce9f538)。Views は coverage 対象外で宣言だけを置く場所なのに、ライフサイクルを持つ AppKit の窓を置いていて、docs の「Views は宣言だけ」とも食い違っていた。移したことで、live の窓を作るテストが coverage に数えられる
  • プレースホルダの頭文字は各語の最初の「文字か数字」にした ([WIP] Ripple が [R だったのを WR に)。色相は NFC に正規化してからハッシュする (アクセントの打ち方だけが違う key は同じ EffectID なのに、色が 2 つになり得た) (5e407ee)
  • docs: AGENTS.md の「Router はメニューの start() / stop() だけを呼ぶ」(ピッカーの Presenter も開閉に合わせて呼ぶ)、HostApplication の「window の無いアプリ」、seeded property test の対象一覧にグリッドの geometry、窓の doc の「⌘W で閉じる」(ハーネスに ⌘W が無い) を直した (bbf403a、ce9f538)

反証された 8 件は、MenuBarRouting が Domain の外の protocol であること、effectPickerPresenter が public であること、画像ケースの読み込み中の扱いなど。いずれも意図した設計か、既にテストで押さえてある。CodeRabbit は指摘なし、Copilot はクォータ不足でレビューしていない。

確認

  • make format (差分なし)、make lint、make check (全スクリプト OK)、make build、make build-dev-harness
  • make test: 1026 tests / 137 suites、known issue 12 件 (すべて FakeGPUGateway などの double の契約テストが出すもので、ピッカーのコードからは 0 件。main の feat: carry the manifest thumbnail through to the effect catalog (#12, data half) #66 が InMemoryFileSystemGateway の fake テストを足して 10 → 12 になった)。main (feat: carry the manifest thumbnail through to the effect catalog (#12, data half) #66, 0.22.0) を merge した後の木で、全 gate をやり直した
  • make test-gpu: 27 tests / 6 suites
  • 足したテスト: EffectPickerGeometryTests、EffectPickerPresenterTests (2 つの Presenter を 1 つの Interactor に載せ、選択が変わるたびにピッカーの選択セルとメニューのチェック行が一致すること、ピッカーが Interactor を start / stop しないことを含む)、AppRouterPickerTests (fake のウィンドウで 1 枚・前に出す・タイトルバーで閉じる・遅れた close・stop()・ピッカーを開いたまま Router を手放す。live の EffectPickerWindow は 1 回だけ作って表示せずに閉じ、自分を報告することを確かめる)、MenuBarPresenterTests に router への受け渡し 2 件
  • CI: preflight / check / lint / test / test-gpu / release-build (macos-15) / codecov/patch がすべて緑
  • codecov/patch は緑 (95.39%、152 行のうち未カバーは 7 行)。未カバーの 7 行は、実行中の application か実際に画面へ出す窓を要る glue だけで、分岐はない: EffectPickerWindow.show() (app を前面にして窓を出す 3 行。テストで呼ぶと CI のランナーに本物の窓が出る)、HarnessAppDelegate の windowWillClose と delegate の設定 (3 行)、AppRouter.init の既定値 AppRouter.pickerWindow(over:onClose:) の 1 行 (live の窓を作る thunk で、窓を作るテストは thunk を通らず直接呼んでいる)
  • 実機の目視 (Lane 1 の Effect picker の行) はまだ

Part of #12

…senters

Move MenuBarPresenter's private StateObservation into CatalogStateObservation, a
class that ends itself when it is let go, so a second Presenter can observe the
EffectInteractor without copying the sink, the newest-wins stream and the
cancellation check.
The picker's cells come from the same EffectInteractor state as the menu
bar, so the chosen cell is always the menu's checked row. The Presenter
only observes: the menu's Presenter owns the Interactor's run, and a
second start() there would restart the catalog. Thumbnails are a
placeholder artwork (monogram and a hue stable across processes) behind
an EffectArtwork enum that real images can join later.

EffectPickerGeometry lives with the Presenters, not the Views, because
Views is coverage-ignored and the column count is logic.

Part of #12
A search field over a grid of placeholder cells, laid out by
EffectPickerGeometry: the chosen cell has an accent border, a broken
package is a disabled cell, and hovering a cell shows its name, author
or reason. EffectPickerWindow hosts the view and reports its own close,
so the Router decides what a close means.

Part of #12
The menu's Presenter hands Browse Effects… to its Router through a weak
MenuBarRouting reference. AppRouter owns the picker: it makes at most one
window through an injected factory, starts the picker's Presenter while
that window is open and stops it when the window closes, from its title
bar or from stop(). A close reported by a window the Router already let
go changes nothing. AppRouter now imports Views to build the window.

The host keeps running when the picker, its only window, closes; the dev
harness still quits when its preview window closes, picker open or not.
The harness's Effects menu is the host's EffectMenu, so it gets the item
too.

Part of #12
ARCHITECTURE.md gains the Effect picker decision (one state seen by two
views, a picker Presenter that only observes, the geometry in
Presenters, the placeholder thumbnail slot, the Router's window
lifecycle, and why there is no live preview yet), the AppRouter -> Views
edge, and the Layer Summary and physical-table rows. AGENTS.md, the
import allow-list, the View-file idiom and Lane 1 of dev verification
say the same.

Part of #12
Copilot AI lite review requested due to automatic review settings October 29, 2026 19:27
@GeneralD GeneralD self-assigned this Oct 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

This change adds a searchable Effects picker, its Presenter and window, and a “Browse Effects…” menu action. The Router manages the picker window and Presenter lifecycle. The picker displays catalog-backed effect cells and forwards selections to the Interactor.

Changes: Effects Picker

Layer / File(s) Summary
Catalog state and picker presentation
Sources/Entity/EffectPickerCell.swift, Sources/Presenters/CatalogStateObservation.swift, Sources/Presenters/EffectPickerText.swift, Sources/Presenters/EffectPickerPresenter.swift, Sources/Presenters/MenuBarPresenter.swift, Sources/Presenters/MenuBarRouting.swift, Sources/Presenters/MenuBarText.swift, Tests/PresentersTests/*, docs/ARCHITECTURE.md
Adds picker cell and artwork data, catalog-state observation, search and display mapping, and the picker Presenter. The menu Presenter can request the picker through a Router protocol. Presenter tests cover filtering, selection, display state, and observation lifecycle.
Picker layout and interaction
Sources/Presenters/EffectPickerGeometry.swift, Sources/Views/EffectArtworkView.swift, Sources/Views/EffectMenu.swift, Sources/Views/EffectPickerCellView.swift, Sources/Views/EffectPickerView.swift, Tests/PresentersTests/EffectPickerGeometryTests.swift, .claude/rules/*, docs/ARCHITECTURE.md
Adds the “Browse Effects…” action, placeholder artwork rendering, searchable grid and cell views, and grid sizing. Geometry tests and project guidance describe the added behavior.
Router and window lifecycle
Sources/AppRouter/*, Sources/App/*, Sources/DevHarness/HarnessAppDelegate.swift, Package.swift, scripts/check-imports.sh, Tests/AppRouterTests/AppRouterPickerTests.swift, AGENTS.md, docs/ARCHITECTURE.md, version.txt
The Router creates, reuses, and closes the picker window, and starts or stops its Presenter with the window lifecycle. App delegates, package dependencies, tests, and architecture documentation reflect the new window behavior.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant EffectMenu
  participant MenuBarPresenter
  participant AppRouter
  participant EffectPickerPresenter
  participant EffectPickerWindow
  participant EffectInteractor
  EffectMenu->>MenuBarPresenter: browseEffects()
  MenuBarPresenter->>AppRouter: openEffectPicker()
  AppRouter->>EffectPickerPresenter: start()
  AppRouter->>EffectPickerWindow: show()
  EffectPickerPresenter->>EffectInteractor: observe catalog state
  EffectInteractor-->>EffectPickerPresenter: catalog state
  EffectPickerPresenter->>EffectInteractor: select effect ID
Loading

Possibly related issues

Possibly related PRs

  • GeneralD/mirage#56: Adds the shared catalog state and selection behavior that this picker observes and uses.
  • GeneralD/mirage#64: Connects the harness menu and app delegate to AppRouter, whose picker-window lifecycle this change extends.
Merge Risk: Minimal · up to c84c5

The picker is mergeable after normal checks; no blocking issue was established.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed 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 describes the main change: a thumbnail-grid effect picker window opened from “Browse Effects…”.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 11 functions across 25 files. (5 skipped: 5 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.39474% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
Sources/AppRouter/EffectPickerWindow.swift 85.00% 3 Missing ⚠️
Sources/DevHarness/HarnessAppDelegate.swift 0.00% 3 Missing ⚠️
Sources/AppRouter/AppRouter.swift 96.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

… hue from NFC

A name like "[WIP] Ripple" gave the monogram "[R"; each word now gives
its first letter or digit, one character upper-cased, and a word with
neither is skipped, so the tile holds at most two characters as the
Entity documents.

Two keys that differ only in how an accent was typed are the same
EffectID (String equality is canonical), but their UTF-8 differed, so
the same effect could get two colours. The hue now hashes the composed
(NFC) form.

Part of #12
Views is coverage-ignored and holds declarations only, but the picker's
window is AppKit glue with a lifecycle: it makes the NSWindow, shows and
closes it and forwards its close. It now sits beside the Router that
opens it, so the test that makes the live window counts toward
coverage, and the docs no longer call Views declarative while listing a
window in it. AppRouter still imports Views for EffectPickerView, the
view the window hosts; the Router tests no longer need Views.

Part of #12
AGENTS.md said the Router calls only the menu's start()/stop(); it also
starts and stops the picker's Presenter with the picker's window, and
only the menu's Presenter starts the Interactor. HostApplication called
the host an app with no window; the picker is its window now. The
seeded property-test targets in ARCHITECTURE.md gain the picker's grid
geometry.

Part of #12
# Conflicts:
#	.claude/rules/dev-verification.md
#	docs/ARCHITECTURE.md
#	version.txt
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.

2 participants