UIレイヤリング(3層パッケージ構成)
このページは実装ではなく設計方針です。
FloatSoda.UI/FloatSoda.UI.Cream/FloatSoda.UI.FizzyPopの 3プロジェクトは、いずれもIsPackable=falseで NuGet に配布していません。 リポジトリにはButtonBase/InteractionState/Button/ButtonStyle/ 各テーマの型が置いてありますが、 骨組みだけで押下にもホバーにも反応しません(実装状況を参照)。いま UI を組む場合は、
FloatSoda本体のウィジェットを直接使ってください。 押せるボタンはGestureDetectorで組み立てられます (→ WidgetSystem § 押せるボタンを作る)。 3層構成の提供は Phase 5 の予定です。
FloatSoda の UI 層は、Flutter で起きた「Material ロックイン」(振る舞い層が独立して存在せず、見た目と振る舞いが material パッケージに一体化した問題)を避けるため、3層のパッケージに分割する計画です。
graph TD
Core["FloatSoda<br/>コア + プリミティブウィジェット"]
UI["FloatSoda.UI<br/>ヘッドレス(振る舞いのみ)"]
Cream["FloatSoda.UI.Cream<br/>DS①: レトロ・クリーミー・フラット"]
FizzyPop["FloatSoda.UI.FizzyPop<br/>DS②: 透明感・グラスモーフィズム"]
Core --> UI
UI --> Cream
UI --> FizzyPop
| 層 | パッケージ | 中身 | 提供状況 |
|---|---|---|---|
| プリミティブ | FloatSoda |
RenderObject を持つウィジェットと、見た目の方針を持たない合成ウィジェット(SizedBox, Flex, ColoredBox, Text など) |
✓ NuGet で配布中 |
| ヘッドレス | FloatSoda.UI |
振る舞い・状態機械のみ(ButtonBase, InteractionState)。見た目は builder デリゲートに完全委譲 |
予定(Phase 5) |
| デザインシステム | FloatSoda.UI.Cream / FloatSoda.UI.FizzyPop |
ヘッドレスの状態から見た目へのマッピングと *Style レコード・テーマ |
予定(Phase 5) |
デザインシステム同士は互いに参照しません。下位層はすべて見える「緩いレイヤリング」にします(デザインシステム層はプリミティブを直接使ってよい)。
FloatSoda のプリミティブ層だけが利用できます。上2層は設計を確定させた段階で、実装はこれからです。
| 対象 | 状況 |
|---|---|
FloatSoda(プリミティブ) |
✓ 使える。NuGet で配布中 |
FloatSoda.UI(ButtonBase / InteractionState) |
予定。型は存在するが ButtonBase が GestureDetector へ未配線で、InteractionState の IsPressed / IsHovered / IsFocused が常に false |
FloatSoda.UI.Cream / FloatSoda.UI.FizzyPop(Button / ButtonStyle / 各テーマ) |
予定。ButtonBase に依存しているため同様に反応しない |
3プロジェクトとも IsPackable=false のため、NuGet パッケージとしては存在しません。
使うにはリポジトリをクローンしてプロジェクト参照を張る必要がありますが、
上記のとおり押下もホバーも動かないため、現時点では実用になりません。
FloatSoda.UI の残作業は Phase 5 のマイルストーンにあります(ButtonBase への GestureDetector 配線が #102、
Cream / FizzyPop の Button 完成が #78 / #100、背景ブラーが #38)。
- Skia / レンダーツリーの型に依存するウィジェットはコア(
FloatSoda)に置く。RenderObjectWidget<T>系は必然的にコア。 - 見た目の方針(意見)を持たない合成ウィジェット(
Center,Containerなど)もコア。Flutter のwidgets層に相当。 - インタラクションの状態機械(pressed / hovered / focused / disabled など)は必ず
FloatSoda.UIに置く。 - 色・余白・角丸などの具体的な見た目はデザインシステム層。
- 振る舞いは必ず FloatSoda.UI に置く。 デザインシステム層の
Stateには「ヘッドレスの状態 → 見た目のマッピング」以外のロジックを書かない。Flutter のTextFieldがmaterialに振る舞いごと実装され、Cupertino が振る舞いを複製する羽目になった轍を踏まないため。 - FloatSoda.UI はデザインシステムの InheritedWidget なしで動作する。 ヘッドレスウィジェットは自前のデフォルトを持ち、
CreamTheme/FizzyPopThemeの存在を前提にしない(Flutter のTheme.of/Material祖先の暗黙要求のようなアンビエント依存を作らない)。
Litmus test: 「2つ目のデザインシステムが、1つ目のコードをコピーせずに同じコンポーネントを作れるか」。Cream と FizzyPop を最初から並走させているのは、この検証を常時行うためです。ヘッドレス層のAPIに片方のデザインシステム固有の都合が漏れたら、もう片方が壊れることで検知できます。
見た目の注入方式
Section titled “見た目の注入方式”Avalonia のルックレスコントロール(疑似クラス + PART_ テンプレートパーツ)の契約を、型付きにした形を採ります:
- 状態の公開 — 文字列の疑似クラスではなく
readonly record struct InteractionState(型付き) - 見た目の注入 — 名前ベースの
PART_検索ではなくrequired Func<IBuildContext, InteractionState, Widget> Builder(型付きスロット、コンパイル時保証)
次のコードは目指す姿であり、いまは押下に反応しません。
// ヘッドレス層(FloatSoda.UI): 振る舞いのみnew ButtonBase{ OnPressed = () => ..., Builder = (ctx, state) => /* state から見た目を構築 */};
// デザインシステム層(FloatSoda.UI.Cream): 状態→見た目のマッピングのみnew Button { Child = new Text("OK"), OnPressed = () => ... };デザインシステム
Section titled “デザインシステム”2つのデザインシステムを最初から並走させる計画です。どちらも Phase 5 で仕上げます。
| Cream | FizzyPop | |
|---|---|---|
| コンセプト | レトロでクリーミーな色使い、フラットデザイン | 透明感、グラスモーフィズム |
| テーマ | CreamTheme |
FizzyPopTheme |
| 現状 | Button + ButtonStyle の骨組みのみ。押下は未反応 |
同構成。加えて背景ブラーが未実装(下記) |
テーマ(XxxTheme.Of(context))はテーマ不在時に null を返し、コンポーネント側が既定スタイルへフォールバックします。テーマが無くても動くことを規約にします。
ロードマップ
Section titled “ロードマップ”主要ヘッドレスUIライブラリ(Radix UI, Headless UI, React Aria, Ark UI, Base UI)の収録コンポーネントを横断調査すると、提供物は2層に分解できる: Tier 1(分解不能な原始インタラクション) と、Tier 2(Tier 1 + Overlay の組み合わせでできる複合コンポーネント)。この構造をそのままヘッドレス層の実装順に採用する。
0. ジェスチャ・ヒットテスト(前提条件) — 充足済み
Section titled “0. ジェスチャ・ヒットテスト(前提条件) — 充足済み”すべての Tier 1 コンポーネントが依存する基盤。コア側では実装済みで、GestureDetector / Listener /
PointerRegion によって press / hover を受け取れる(→ WidgetSystem § ジェスチャとヒットテスト)。
残る制約は2つある。
- ポインタ座標が届くのはダッシュボードオーバーレイだけ。他のオーバーレイ種別への接続は Phase 1 の残件
- フォーカスの概念はまだ存在しない。
InteractionState.IsFocusedを埋める仕組みは未設計
1. Tier 1 — 原始インタラクション
Section titled “1. Tier 1 — 原始インタラクション”分解不能なインタラクションモデルを1つずつ実装する。各モデルは既存コードとの重複がないことを確認済み:
| 順序 | コンポーネント | インタラクションモデル | 備考 |
|---|---|---|---|
| 1 | ButtonBase |
単発アクション(press) | 型と Builder スロットは実装済み。ButtonBaseState から GestureDetector への配線が未了で、押下・ホバー状態がまだ更新されない |
| 2 | ToggleBase |
二値切替(on/off) | Checkbox・Switch の共通基盤 |
| 3 | RadioGroupBase |
排他選択(択一) | Tabs の選択状態管理とも共有可能 |
| 4 | SliderBase |
連続値(ドラッグ) | |
| 5 | TextFieldBase |
文字入力 | 「振る舞いの一部が見た目」(カーソル・選択ハンドル)になる最難関。builder / スロットで見た目を外注する設計をここでも貫く |
| 6 | CollapsibleBase |
開閉(表示・非表示) | Accordion の共通基盤 |
2. Overlay / Positioning primitive(Tier 1 と並ぶ独立コンポーネント)
Section titled “2. Overlay / Positioning primitive(Tier 1 と並ぶ独立コンポーネント)”Menu・Select・Combobox・DatePicker・Tooltip・ContextMenu など Tier 2 の大半が同じ Popover 実装を使い回している。HTML/CSS の世界には対応要素がなく、VRオーバーレイでは「アンカー要素に対して浮遊パネルを3D空間にどう配置するか」(画面外にはみ出ない、他ウィンドウと重ならない、視線方向を考慮する)が SteamVR 特有の難問になるため、Web版ヘッドレスUIの実装をそのまま輸入できない。Tier 2 全体をブロックする基盤なので、Tier 1 と並行して早期に着手する。
3. Tier 2 — 複合コンポーネント
Section titled “3. Tier 2 — 複合コンポーネント”Tier 1 + Overlay の組み合わせで実装し、状態機械を個別に再発明しない(Litmus test と同じ規律):
| コンポーネント | 組み合わせ元 |
|---|---|
SelectBase / ComboboxBase |
TextFieldBase(検索) + リスト選択 + Overlay |
MenuBase |
Overlay + キーボードナビゲーション + ButtonBase 群 |
AccordionBase |
CollapsibleBase × 複数 + 排他制御(RadioGroupBase と同じ択一ロジック) |
TabsBase |
RadioGroupBase の選択状態管理を流用、見た目のみ異なる |
4. FizzyPop の完成に必要なレンダー機能
Section titled “4. FizzyPop の完成に必要なレンダー機能”グラスモーフィズムの背景ブラーには BackdropFilter 相当(SkiaSharp の SKImageFilter.CreateBlur を使うレイヤー / RenderObject)が必要。現状は半透明ベタ塗りまで。Tier 1/2 の実装とは独立して進行可能。
- Architecture — アセンブリ構成と3ツリーモデル
- WidgetSystem — 組み込みウィジェット一覧
- APIDesign —
*Styleレコード分離などのAPI規約