パスの基点: 本文の相対パスは engine ルート基準 (krkrz_dev では src/core/ を前置。下記参照)。作業ディレクトリが krkrz_dev 以外 (krkrz_android / krkrz_ios などの外枠や案件フォルダ) のときは ${KRKRZ_BASE}/krkrz_dev/ を前置して読む (echo $KRKRZ_BASE で実パスを確認。マシンごとに値が違うので絶対パスは書き込まない)。
Elements ベース ダイアログ / 画面 UI (krkrz)
吉里吉里Z に埋め込んだ Elements (ThorVG/cycfi ベースの C++ GUI) で、JSON / TJS Dictionary 定義のダイアログや画面を出す仕組み。ElementsDialog クラス経由で使う。全デスクトップ変種 (SDL3 / WINVER / OGL) で動作する。
Win32 の WIN32Dialog(win32dialog プラグイン)とは別物。あちらは Win32 ネイティブダイアログ。こちらは全 PF 共通の Elements UI。
パスの読み方
本文のエンジンパスは engine ルート相対 (common/..., data/..., doc/..., external/...)。
本スキルが置かれている umbrella (krkrz_dev) では src/core/ を前置して読む
(例: src/core/doc/ElementsDialog.md、src/core/data/ui/、src/core/external/elements/)。
engine 単独チェックアウトならそのまま。
ゲーム (KAG/kag) へ組み込むときは
references/krkr_integration.md を読む — 案件へ載せるときの統合知識: 層構成 (基盤 TJS + 画面ドライバ) / 表示経路の選択と engine 側の制約 (overlay のサイズ上限・showFile が autopath に乗らない・サブクラスの super.ElementsDialog()) / KAG の openDialog 枠へ乗せるホストレイヤ方式と z オーダー / ドライバの型とライフサイクルの落とし穴 (常駐画面の再表示・タブ切替) / 値と動的画像の注入 / 資材探索とフォント一括登録 / 遷移エフェクト / Agent 検証の癖 / 未解決の engine TODO。
最初に読むべき一次資料 (SSOT)
doc/ElementsDialog.md — 機構全体・API 経路・入力ルーティング・複数インスタンス・フェーズ状態。まずこれ。
external/elements/external/elements_modal/README.md — JSON レイアウトの全ウィジェット/属性カタログ(下の一覧は要約)。
- 実例:
data/ui/about.json / data/ui/menu/*.json + data/ui/menu/app.jsonc(navigator フロー) / data/elements_gallery/ / data/transition_demo/ / data/startup.tjs(FlowMenuDialog 常駐メニュー)。
1. 基本的なダイアログの作り方 (最小例)
ElementsDialog を継承し、onAction でボタン等に反応、showJson(または showDict)で表示、close() で閉じる。非モーダル (オーバーレイ) が基本形。
class MyDialog extends ElementsDialog {
function onAction(id, payload) {
// state widget の値変化 / button click で発火 (payload: 値、button は void)
switch (id) {
case "ok": System.inform("OK が押されました"); close(); break;
case "cancel": close(); break;
}
}
}
var dlg = new MyDialog();
dlg.showJson('{
"size": [360, 200],
"background": [30, 30, 60, 245],
"input": { "arrow_focus_nav": true },
"content": { "type": "margin", "padding": 20, "child": {
"type": "vtile", "children": [
{ "type": "align_center",
"child": { "type": "label", "text": "こんにちは", "size_scale": 1.3 } },
{ "type": "vspacer", "height": 20 },
{ "type": "htile", "children": [
{ "type": "button", "id": "ok", "text": "OK", "initial_focus": true },
{ "type": "hspacer", "width": 12 },
{ "type": "button", "id": "cancel", "text": "キャンセル" }
] }
]
} }
}');
Dictionary で書く (showDict) — JSON 文字列を書かなくて済む
common/visual/elements/VariantJsonUtil.cpp が Dictionary→JSON へ変換してから同じ経路に流す。JSON をエスケープせずに書けて可読性が高い。
dlg.showDict(%[
size: [360, 200], background: [30, 30, 60, 245],
input: %[ arrow_focus_nav: true ],
content: %[ type: "margin", padding: 20, child: %[
type: "vtile", children: [
%[ type: "label", text: "こんにちは", size_scale: 1.3 ],
%[ type: "vsize", height: 40,
child: %[ type: "button", id: "ok", text: "OK", close_on_click: true ] ],
] ] ]
]);
ElementsDialog.dictToJson(value) で変換結果 JSON を取得できる (デバッグ / 資材書き出し)。
- TJS Dictionary の制約 2 点(重要):
- bool が無い:
true=整数1。close_on_click: true は JSON の 1 になる (elements 側は 0/非0 を真偽として受ける)。
- 空文字キー不可: navigator の既定遷移
"": "<exit>" は Dict で書けない → 未定義 action フォールバック(entry=exit / 子画面=pop)で足りるか、その画面だけ JSON 文字列にする。
2. レイアウト JSON スキーマ
トップレベル
| キー | 型 | 説明 |
|---|
size | [w,h] | 希望論理サイズ(上限)。実サイズは content の自然サイズにフィット縮小(上余白対策) |
background | [r,g,b,a] | content 描画前の塗り(省略=透明・縁なし) |
align | "center"(既定)/top/bottom/left/right/top_left/bottom_right 等 | overlay 上の配置。入力座標補正も同じなのでクリックずれ無し。全 overlay 経路で有効 |
margin | int | 非中央側のサーフェス端からの余白 px(既定 0)。例: 左上メニュー = "align":"top_left","margin":24 |
locale | "ja-JP" 等 | label の locale 未指定時の既定(CJK 同形字の出し分け) |
content | element | ルート要素 |
input | object | キー/パッドナビ設定(§6) |
vars | {name:string} | 変数 store 初期値(label.text_var / vars_on_focus が参照) |
transitions | {action:target} | navigator の遷移定義(§5)。object 形式で画面切替エフェクト(effect/duration/rule/vague)も宣言可 |
pad_theme | xbox/ps/switch/keyboard/none | pad_icon の名前解決 |
atlases | {name:spec} | テクスチャアトラス事前ロード(atlas_* が参照) |
font_scale | number | 明示 size を持たない button/toggle/check_box/label の既定フォント倍率(既定 1.0) |
style | object | レイアウト密度の一括指定: %[font_scale, tile_gap(gap未指定タイルの既定), row_height(button系/input_box/selection_menu の既定最小高), padding(content外側余白)]。全て省略=従来一致。既定の「詰まった」見た目を spacer なしで解消できる |
ウィジェット ("type") 要約 (全リストは elements_modal/README.md)
レイアウト: vtile/htile(children,gap=子間隙間px・省略時 style.tile_gap→0) / margin(padding,child) / hsize/vsize(width|height,child) / hspacer/vspacer(width|height) / spacer(size) / align_center/align_left/align_right/align_top/align_middle/align_bottom/align_center_middle(child) / box(color) / band(color+任意child) / layer(children,先頭=最前) / group(title,child) / scroller(child) / filler / floating(at:[x,y,w,h],child) / canvas(children に at 付き、PSD 絶対配置向け。choice_nav:true で子 choice 群を 1 フォーカスの左右トグル化、子の at_var で配置を変数駆動)。
注意: hsize/vsize の指定値は child の limits に clamp される (cycfi fixed_size 仕様)。label 等の固定 max を持つ child を直接包んでも希望サイズまで広がらない → 全体サイズを確定したい画面は各パーツ幅を明示して自然サイズ=希望値にするのが確実。
長文テキスト: text_box — 複数行・自動折返しの静的テキスト (text,size|size_scale,color,mono=等幅,text_var=setVar で本文丸ごと差替え)。幅は親 (hsize) が決め、高さは折返しに追従。長文は親に scroller。行 label 大量生成より軽い。実例=data/ui/license_dialog.tjs (showLicenseDialog: 左=一覧/右=text_box の 2 ペイン全画面モーダル、モーダル中も onAction→setVar が同期で効く)。
矩形テキスト/字幕: text_area — 矩形へ流し込む静的テキスト (text/text_id/text_var/text_list_id+index_var は label と同規約, size|size_scale, color, font=comma区切りfamilies, align=left/center/right, line_spacing=行間追加px, base=auto/ltr/rtl, count_var=文字送り (-1=全部))。折返し・行頭行末禁則・count が本体 Layer.drawShapedTextArea と同一ロジックなので同じ本文・同じ幅なら改行位置が一致する (glyphware layoutBlock)。折返しは全文で確定してから count を適用=送ってもリフローしない。数える単位はクラスタ (Layer.shapedTextCount と同じ)。text_box は従来互換 (素朴な折返し・禁則なし) で据置なので既存画面の改行は変わらない。⚠floating の絶対座標で置くなら top-level "size":[w,h] を必ず書く (省略するとダイアログが内容最小サイズまで縮み何も見えない)。仕様=elements docs/block-text.md。
入力/state: label(text,size=px絶対 or size_scale=倍率,color,text_var,text_id=i18n,text_list+index_var=指定番号表示、index+index_offset_var=«N 行の窓»〔行ごと固定 index + 行共有の先頭位置。引く位置=index+offset、窓モードの範囲外は空文字〕、text_list_var=一覧データ自体の差替え) / button(text,id) / checkbox|check_box(text,id,value) / toggle_button / input_box(placeholder,id,size,max_chars=最大文字数〔codepoint 単位、別名 maxlength、0/省略=無制限。満杯打鍵は無視・paste は収まる分だけ〕,text/value=初期値〔build 時全選択=そのまま打つと置き換え〕) / selection_menu(id,options,selected) / slider(0..1 の素のスライダ。id,initial,vertical,thumb_color/track_color〔既定=テーマ予約色 @slider_thumb 白 / @slider_track 黒〕,value_var,display/display_var) / invert_button / ring_button / labeled_row(label,label_width,child) / tab_view(tabs,initial) / picker 系 cycle_picker/framed_cycle_picker/segmented_picker(options|options_id=i18n,initial,font_size,font=表示テキストの family[#axes]・省略でテーマ既定,index_var=選択indexを変数へ)。
装飾/画像: pad_icon(コントローラアイコン) / sprite_button / atlas_image(rect or rect_list+index_var=変数で矩形切替、focus_link=リンク先 id のフォーカスで frames.normal/frames.hilite を切替える飾り〔normal 省略可=非フォーカス時は何も描かない・素材1枚のフォーカスインジケータ用。既定で「飾りに hover→リンク先へフォーカス」プロキシが付きクリックも奪うので、コントロールに重なる配置は "hover_focus_link": false〕)/atlas_button/atlas_toggle/atlas_choice(排他)/atlas_slider(thumb 形式 or fill+fill_at ゲージ形式、value_var。両端の増減矢印=dec/inc+dec_at/inc_at〔+track_at/step/repeat*/flash_ms、名前は減/増の意味で幾何名 left/right/up/down はエイリアス。atlas_scrollbar も同キー〕)/atlas_progress/atlas_cycle_picker(画像矢印ピッカー、フォーカス=矢印hilite、font 指定可)/animated_sprite / gizmo_image(9patch)。
- 色は
[r,g,b,a] 配列(0–255)。フォントサイズは size=px絶対 / size_scale=倍率(テーマ既定≒14px 比)。JSON/JSONC(コメント+末尾カンマ)可。
- i18n: top-level
strings({id:{lang:str}}) + lang。text_id/options_id/text_list_id が現在言語で解決される。TJS からの実行中切替は ElementsDialog.language = "en"(表示中の全ダイアログへ即時反映・開き直し不要、picker は選択 index 維持。以後開く画面の既定にもなる)。
- 変数連動: picker
index_var(双方向: 選択変更で書き + setVar で quiet 追従) ↔ text_list/rect_list の index_var(読み) を同名にすると選択連動(機種選択→SPEC/スクショ等)。value_var=10進小数、at_var="x,y[,w,h]"。picker enabled_var=選択肢の有効/無効 mask('0'/'1'文字列、step/click が無効 index をスキップ。未開放機種の出し分け)。choice (atlas_choice/radio_button) の selected_var+selected_value=ラジオグループ変数(グループ全員同じ var + 異なる value、var==value の1個が選択、双方向)。TJS からは dlg.setVar(name, value) で駆動。
- 画面をまたいで保つ変数: top-level
"shared_vars": ["cfg_*","ui_lang"](完全一致 or 末尾 * の前方一致)。一致した変数はセッション共有ストアと双方向になり、次の画面へ引き継がれる(共有側の値が画面の "vars" 既定より優先)。ホスト実装不要。セーブ/ロードは static API ElementsDialog.setSharedVar(name,value) / getSharedVars()(辞書)/ clearSharedVars()。共有側へ出るのは「変化として書かれた」値だけで widget の初期値(initial/value)は出ない。設定画面をタブで渡り歩いてスライダーが戻る、を防ぐのが本来の用途。
- 2 値トグルの
value_var: checkbox/check_box/toggle_button/slide_switch が変数 store と双方向(""/"0"/"false"=off)。未設定なら "value" を種まき、既にあればそれで初期状態を上書き。setVar での追従では on_click は発火しない。設定 ON/OFF がホストのコールバック無しで書ける。
- 絵を変数で差す
image_var: image の絵そのものを変数で差し替える。値がそのまま画像パス("resources/x.png" / "mem://thumb_3" / 空=無描画)。構築時は変数値が静的 "image" より優先、未設定なら "image" を種まき。セーブ一覧のページ送りでサムネが変わる / CG ビュワーの絵を送るが画面再構築なしで書ける。空や読めないパスでも widget は残るので、正しいパスを入れれば戻る。
- 差し替え可能アトラス: top-level
"atlases": { "cg": { "path": "atlas/cg_g0.png", "swappable": true } } と宣言すると、ElementsDialog.setAtlasImage("cg","atlas/cg_g1.png")(static、path は画面の resource_base 起点、戻り値=差し替えたか)で画面はそのまま絵の束だけ入れ替わる。widget は作り直さないのでレイアウト/フォーカス維持。⚠同じ矩形割りであること(frames/rect は変わらないので位置がずれると別の絵が出る)。一覧は ElementsDialog.swappableAtlases()。swappable はパス単位キャッシュに乗らない(他画面を巻き添えにしないため)。
- モーダルへの初期 vars 注入:
dlg.showModalFile(path, %[name=>value,...])(showModalJson/Dict も同様、第2引数 Dictionary)。build 直後・pump 前に変数 store へ流し込む(モーダル中は TJS がブロックされ setVar 不可のため)。モーダル中も onAction は同期で届く。
- pad_icon/フォント setup (static):
ElementsDialog.setPadIconBase(dir)(Kenney SVG のベース storage パス。未設定だと灰色プレースホルダ)/ElementsDialog.setPadTheme("xbox"|"ps"|"switch"|"keyboard"|"auto")("auto"=接続パッドの系統〔System.padStyle〕から自動選択。接続数/系統の変化をその場で検出して決め直すので抜き差しに即追従〔表示中の画面も再描画〕。系統不明のプラットフォームはパッド 1 台以上で "xbox"、0 台で "keyboard")/ElementsDialog.setPadIconAlias(theme, name, basename)(論理名→Kenney basename の既定表〔a=Enter/b=Esc/dpad=矢印〕をテーマ単位で上書き。例: キャンセルが BackSpace なら setPadIconAlias("keyboard","b","keyboard_backspace")。basename 空で個別解除、name 空でテーマ全解除)/ElementsDialog.registerFontDir(dir)/ElementsDialog.defaultFontFamily = "Open Sans, Roboto, Noto Sans JP, ..."(明示設定は自動 theme 並びに上書きされない。Emoji 系は必ず末尾に)。
- 要素の有効/無効を変数連動: button 系(
button/atlas_button/invert_button/ring_button)の enabled_var。値 "0" で無効、それ以外(既定)で有効。無効中はクリック/キー決定が効かず、描画は disabled frame があればそれ、無ければ半透明。進行で開放されるメニュー項目(未クリアなら「おまけ」を灰色)等に。
static 設定一覧 (クラス全体に効く。ElementsDialog.xxx)
⚠ ElementsDialog を継承したクラスのメソッド内から触るときは global.ElementsDialog.xxx。素の ElementsDialog は親クラス参照になり、static プロパティへの代入が「メンバが見つかりません」になる。
| プロパティ | 既定 | 用途 |
|---|
language | "" | i18n 表示言語。代入で表示中の全画面へ即時反映(上記 i18n 参照) |
focusRing | false | フォーカス中要素に描かれる汎用の枠(青い角丸)。krkrz ホスト初期化で OFF が既定(authored 画面は focused frame / focus_link 装飾で表現する方針)。使いたい場合に true(起動時の明示設定は初期化に上書きされない)。button/slider/dial/thumbwheel が対象。OFF でもフォーカス自体は生きるのでキー/パッド操作と hilite 切替は不変 |
renderCache | true | 変化の無いフレームの再ラスタ+再アップロードを省略。アイドルがゼロコストになる。false は負荷比較用 |
partialRedraw | true | 変化した矩形だけ描き直す(ダーティ矩形)。false は全面 |
baseSize | 未設定 | UI の author 基準面サイズ [w,h](overlay 拡縮 fit の分母)。void で既定=ゲーム基準面(primaryLayer)へ戻る。ゲーム画面と別解像度で UI を author しているときに設定すると、部分パネルの拡縮が author 基準になりゲーム側の基準面変更に巻き込まれない |
renderScale | 0 | ラスタライズ密度。0=auto(present サイズで直接)/>0=authored×倍率で描いて拡縮 |
renderStats / renderStatsReset() | — | 描画パイプラインの区間計測(frames/rasters/partials/updateUs/rasterUs/uploadUs/presentUs 等)。累積値なので2回読んで差分を取る。計測画面=data/elements_bench(-benchauto で無操作スイープ) |
renderCount | — | 累計ラスタライズ回数。アイドルで増えなければ renderCache が効いている |
atlasCacheStats | — | アトラスのデコードキャッシュの常駐量 %[bytes, count, budget]。画面を閉じても手放さない設計なので、場面の切れ目で抱え込み量を見る用 |
trimAtlasCache(budget=0) | — | アトラスキャッシュを切り詰める(0=使われていない分を全部)。戻り値=解放バイト数。⚠表示中の画面が使っている分は参照が残るので落ちない→画面を閉じた後に呼ぶ |
atlasCacheBudget | 192MB | アトラスキャッシュの予算。代入で恒久変更(下げたらその場で切り詰め)。0 でキャッシュ無効 |
interactive 属性 (focusable widget 共通)
"id" — onAction / result.values / shortcut / setVar の参照キー。
"initial_focus": true — 起動時フォーカス候補。複数指定可で、先頭候補が enabled_var で無効なら次の有効候補へ落ちる。数値を書くと明示優先度(小さいほど優先・true=0、同値は build 順)。候補確定は表示直後の idle まで遅延するので、show 後に setVar で有効/無効を注入する運用でも注入後の状態で判定される。
"close_on_click": true — 既定 false。true の button だけが click で「閉じて確定」する(result.action=id)。false は onAction を発火するだけで閉じない → OK/Cancel 等の「閉じるボタン」にだけ付ける。
3. 表示経路の使い分け (ElementsDialog メソッド)
| メソッド | モード | 用途 |
|---|
showJson(json[, grabFocus=true[, modal=grabFocus]]) / showFile(path, ...) | overlay・非ブロッキング | 通常のダイアログ。onAction 逐次、close() で終了。showJson(json, true, false)=非モーダル+フォーカスあり(操作パネル推奨形) |
showDict(dict, ...) | 同上の Dictionary 版 | JSON を書かずに |
showModalJson(json) / showModalDict(dict) | overlay モーダル・ブロッキング | ゲーム画面上に nested ループ。戻り値 %[action, values] |
showModalJson(json, title, w, h) | 独立 OS ウィンドウモーダル・ブロッキング | 別ウィンドウ。SDL=run_modal / WINVER=専用 Win32 窓 |
showFlow(manifest) / showFlowScreens(dict, entry) | overlay・ブロッキング・複数画面 | navigator フロー(§5) |
startFlow(manifest) / startFlowScreens(dict, entry) | overlay・非ブロッキング・常駐 | 出しっぱなしメニュー。背景動作と併存(§5) |
- ブロッキング(showModal/showFlow)* は
result を返す: result.action=閉じた button id(Esc/× は "")、result.values=state widget 最終値マップ。
showFile/showFlow のパスは Storages 解決だが autopath 検索には乗らない — ファイル名だけだと失敗するのでパス付き("system/foo.jsonc")で渡す。画面ファイルの相対資材はその画面ファイルのディレクトリ起点。
ホストのレイヤへ描く: ElementsPanel (overlay と別枠)
new ElementsPanel(layer) + showFile/showJson/showDict で、同じ画面 JSON を Layer のビットマップへ描く。z 順・[trans]・piledCopy・入力の帰属がレイヤの仕組みに従うので、本文の上に被らない HUD / トランジションに乗せたい画面 / KAG レイヤ z 順に混ぜたい画面はこちら (overlay は常に最前面)。
var lay = new Layer(win, win.primaryLayer);
lay.setImageSize(400, 300); lay.setSizeToImageSize();
lay.type = ltAlpha; lay.hitThreshold = 0; lay.visible = true;
var panel = new ElementsPanel(lay);
panel.onAction = function(id, payload) { ... };
panel.showFile("ui/hud.jsonc");
// 入力はレイヤから手で流す (座標はレイヤ local をそのまま)
lay.onMouseDown = function(x, y, b, f) { panel.mouseDown(x, y, b, f); };
lay.onMouseUp = function(x, y, b, f) { panel.mouseUp(x, y, b, f); };
lay.onMouseMove = function(x, y, f) { panel.mouseMove(x, y, f); };
- イベント / 変数 API は ElementsDialog と同形 (
onAction/onDrag/onVar/onClose/setVar/getVar/listVars/watchVars/focus/activate)。既存ドライバがほぼそのまま載る。
- 持っていないもの: モーダル結果 / navigator フロー / 仮想キーボード / ホストホットキー表 /
Agent.dialogs() にも現れない。1 パネル=1 画面、切替は showJson 呼び直し (クロスフェードはレイヤ 2 枚+[trans])。
- キー/パッドは既定で受けない。効かせたい画面だけ
keyDown/keyUp/text を流す (戻り値=消費したか)。
- レイヤの画像サイズを変えたら
notifyResized()。invalidate() は明示全面再描画 (通常不要)。
- テーマ / 表示言語 /
registerImage の mem:// ストアはプロセス全体で共有 (パネルにも即時反映)。
4. イベント (ElementsDialog をサブクラスして override)
class D extends ElementsDialog {
function onAction(id, payload) { /* 値変化 / click。payload=値(buttonはvoid) */ }
function onClose(action) { /* teardown 完了。action=閉じた button id(外部要因は空) */ }
function onScreen(name) { /* フロー: 画面 enter */ }
function onScreenLeave(name, act) { /* フロー: 画面 leave */ }
function onDrag(e) { /* ドラッグ。e=%[id,phase,x,y,dx,dy,startX,startY,modifiers] */ }
}
onAction は 全 button click と state widget 値変化で発火(TVPPostEvent 経由)。
onDrag は画面 JSON で "drag_events": true を書いた widget の 押下→移動→離す で発火。
e.phase は "begin" / "move" / "end"(TJS Dictionary に bool が無いので文字列)。
座標は画面 JSON の座標系。溜まった move は最新 1 件へ畳まれる(begin/end は畳まない)。
⚠ 掴んだ絵をついてこさせるだけなら onDrag は要らない: widget に
"drag_at_var": "名前" を書くと位置が "x,y" で変数へ書かれ、canvas 子の "at_var" に
同じ変数を挿すだけで追従する(C++ 内で完結しフレーム同期)。"drag_bounds": [x,y,w,h] で
可動域も制限できる。onDrag は「どこで離したか」等の判断用。
- サブクラスは必ず
super.ElementsDialog() を呼ぶ(コンストラクタ)。呼ばないと native インスタンスが未初期化になる。
- ホスト→UI の値反映:
dlg.setVar(name, value)(vars/text_var を subscribe した label が次フレームで更新)。ソフトキーボードの入力表示等。
- フォーカスをプログラムで移す:
dlg.focus(id)(input_box は編集フォーカス=キャレット+text 受理。at_var の park/unpark で画面を組み替えた後の入力先移動用。id の存在確認はしない/非アクティブなら false)。initial_focus / focus_by_id が input_box に「一度クリックするまで」効かなかった問題は修正済み。
5. 複数画面フロー (navigator) と 常駐メニュー
1 つの overlay 上で複数画面 (JSON) を遷移。各画面の top-level "transitions" が「閉じトリガ action id → 次手」を定義:
- target 語彙:
"name"(push・山括弧不要)/ "<back>"(pop)/ "<replace:name>" / "<stay>" / "<exit>"(または空)。未定義 action は entry=exit / 子=pop にフォールバック。
- 画面遷移する button は
close_on_click:true + transitions。その場で動く(閉じない) button は close_on_click 無し(onAction のみ)。
- 画面切替エフェクト (krkrz overlay 配線済・CPU 合成で全 DrawDevice 同一動作): entry を object 形式にして
%[target:"s2", effect:"fade", duration:300] / %[target:"<back>", effect:"universal", rule:"rule.png", vague:64, duration:500]。rule はグレースケール画像(値が小さい画素から先に次画面へ)、解決順=宣言した画面の相対→Storages→autopath。未対応 effect / rule 不在は警告+即切替(rule 不在は fade フォールバック)。デモ: src/core/data/elements_flow/。
- 退場(exit)演出: 要素の
"animate" に "on":"exit" を付けると、閉じ/遷移時に演出を再生してから finish する(session 内自動協調)。TJS ElementsDialog.close() でも発火(演出完了後に閉じ、transitions は解決せずフロー終了)。"animate" は move/scale/rotate/fade を from/to/frames/easing で指定するパーツ演出(トリガ on: enter(既定)/focus/select/exit/hover/change)。
// ブロッキング複数画面: マニフェスト or インライン辞書
var r = dlg.showFlow("ui/app.jsonc");
var r = dlg.showFlowScreens(%[ "menu": '{...}', "settings": '{...}' ], "menu");
// 常駐(非ブロッキング)メニュー: 出しっぱなしで背景動作と併存
dlg.startFlow("ui/menu/app.jsonc"); // 即 return(戻り値=起動成否)
// dlg.active … この dlg が今アクティブか(getter) dlg.close() … 閉じる
実例: data/startup.tjs の FlowMenuDialog + data/ui/menu/*.json。
6. 入力・フォーカス・モーダル・複数インスタンス
- 配送優先順位:
最上位ホットキー(System.registerHotKey・ポンプ入口) > モーダル(全消費) > ホストホットキー(ElementsDialog.registerHotKey・バイパス) > フォーカスパネル(handled素通し) > ゲーム。
- 複数インスタンス同時表示 OK(z-order。先頭=最背面/末尾=最前面)。各インスタンスは
modal フラグを持つ。
modal=true(showJson 既定/showModal*/showFlow): 全入力を独占(下・ゲームに通さない)。
modal=false(startFlow/startFlowScreens、showJson 系は第3引数で指定可): ヒットしない入力は下/ゲームへ素通し。
- 用途 3 態: モーダル
showJson(json) / 操作パネル showJson(json, true, false)(キー/パッドがパネルへ届き、未処理分はホストへ素通し。パッド十字=フォーカスナビ/A=決定) / 表示専用 HUD showJson(json, false)(キーを一切受けない)。
- キーボードフォーカス: modal または
wants_focus の最前面が保持。後から開いた focus-grab が自然に前面、閉じると直前へ戻る。テキスト入力ウィジェット focus 中は grabFocus=false でもキー/テキストが届く(focus_consumes_text フォールバック)。
- 最上位ホットキー
System.registerHotKey(key, mods, callback) / System.unregisterHotKey(key, mods): イベントポンプ入口で照合するので モーダル表示中・テキスト入力中でも効く唯一の層(下の ElementsDialog.registerHotKey より上流)。callback が false を返せば消費せず通常 dispatch へ素通し。リピートは消費のみ・up は key のみ照合。モーダルの有無は ElementsDialog.modalActive(読取専用・常駐オーバレイは含まない)で判定。WINVER / SDL3 の両ビルドで動く(WINVER は 2026-09-26 に配線)。
- ホストホットキー
ElementsDialog.registerHotKey(key, shift=0, duringTextInput=false) / unregisterHotKey / clearHotKeys: 登録キー(VK_PAD*・VK_RBUTTON 等マウスも同じ空間)はパネルへ渡らず Window.onKeyDown/onMouseDown へ直行(バイパス方式・専用イベント無し)。テキスト入力中は既定抑止(duringTextInput=true で有効)。モーダル中は無効。ESC/PgUp/PgDn 等「シェルが必ず受けたいキー」の確保に使う(実例=demolib DemoShell)。
input(top-level)で矢印/パッドナビ(arrow_focus_nav / dpad_mode / shortcuts〔key/pad→id〕/ pad_bindings)を設定。既定 bind: A=Enter / B=Esc / X=Shift+Tab / Y=Tab / D-Pad=矢印。"bindings": [{key|pad|mouse|wheel, action}] で named-action を差替("none"=消費して無効化、"passthrough"=消費せずホストへ素通し=常駐オーバレイが「この入力は下のゲームのもの」と宣言する用)。pad 名はフェイス以外に "lb"("l1")/"rb"("r1")/"lt"("l2"/"lt_click")/"rt"("r2"/"rt_click")/"l3"/"r3"/"back"/"start"/"dpad_*" が使える(トリガ 2 つは krkrz 側の VK_PAD7/VK_PAD8 変換が入って初めて届くようになった)。フェイスボタンは刻印("a"/"b"/"x"/"y")と位置("face_south"/"face_east"/"face_west"/"face_north")の 2 系統で、1 押下で両方届く。表示側 pad_icon の name も同 2 系統を持つので割り当てと表示は同じ基準で組にする(任天堂系は X/Y の位置が Xbox と逆)。
- 一覧は
list で組む(行テンプレート): "rows": N + "row": {…木…} + "row_size"/"pitch"。文字列値の #index が行番号へ展開され("id": "row#index" / "visible_var": "rhov#index")、text_list_var 等を持つ要素には "index" と "index_offset_var" が自動で挿さる(= «窓» になる)。行の中の widget が先にクリックを受け、誰も受けなければ行クリックとして onAction(id, データindex)。count_var を渡すとデータが無い行は描画も当たりも消える。hover/選択は行ごとフラグ(row_hover_var/row_select_var → visible_var で受ける)か、ホスト側で拾う hover_var/select_var(onVar)。
- スクロールバーは
atlas_scrollbar: "thumb"(9-slice 可)+ "track"(省略可)を index_offset_var(+count/count_var、見え行数は visible 数値 or visible_count_var〔⚠旧 visible_var から改名: 全 widget 共通の表示/非表示キーと衝突するため〕)か value_var へ直結。つまみ長は「見えている行数 ÷ 総件数」に比例し、溝クリックでページ送り・ホイール・ドラッグまで内蔵。list と同じ index_offset_var を挿すだけで連動する。両端の増減ボタンは dec/inc(atlas_slider と同キー、送り量=ホイールと同じ)。
- 1px 単位で滑らかに送る一覧は
virtual_scroller: 窓に収まる行数 + 1 行ぶんのセルを子 canvas に row_height 間隔で並べ、row_height(必須)/rows_visible/row_count_var(総行数)/top_row_var(先頭行。onAction(id, 先頭行) も出る)/pos_var(0..1 双方向)を書く。セルが «窓»(index+index_offset_var)で一覧を引いていれば top_row_var と同じ変数を挿すだけで中身が付いてくる(ホスト実装なし)。ホストが絵を差し替える場合は差し替え後に row_ready_var へ同じ行を書く(書くまで位置をずらさず待つ)。ホイール/PageUp/PageDown/Home/End はアニメーション、drag_scroll: true でドラッグ+慣性、窓の端の行での ↑↓ は 1 行送り(フォーカスは同じセル)。スクロールバーは atlas_scrollbar を value モード(value_var=一覧の pos_var)+ row_steps: true(送りを行単位に)+ active_var(掴んでいる間 "1")にし、その変数を一覧の bar_active_var へ(離すと行へ揃う)。縦 atlas_slider を代わりにするなら一覧に pos_reverse: true。詳細は elements_modal README「スムーズスクロール一覧」。
- スライダーの目盛制約
"snap": true(slider/slider_with_range/atlas_slider 共通): 値を 1 目盛の倍数にしか止めない。ドラッグ中も目盛から目盛へ飛び、キー/ホイール/パッド/読み上げも 1 目盛ずつ。1 目盛 = "step"(0..1)→ "display" の step/(max-min) → 5%。atlas_slider の "active_var" は押している間 "1"。
- つまみ資材の 9-slice / 1 変数で複数枚:
atlas_slider/atlas_scrollbar の "thumb" は {"rect":…,"insets":[l,t,r,b],"size":[w,h]} でキャップを潰さず伸ばせる。canvas 子は "at_var_offset": [dx,dy,dw,dh] で 1 本の at_var から複数枚(上キャップ+胴+下キャップ等)を動かせる。
- 変数は読める / 変化も拾える:
setVar の逆向きに getVar(name)(未知は void)、一覧は listVars()(name/value/usedBy=%[id,kind])。変化通知は onVar(name, value) を実装したダイアログだけに届く(未実装ならコスト無し)。vars_on_hover / vars_on_focus / slider の value_var / drag_at_var / 一覧の index_offset_var など画面側が書いた値も同じ storeなので全部読める → 「絵はホスト側のレイヤ、当たり判定だけ elements」構成で hover をホスト処理へ繋げる。通知は 1 フレーム遅延+同名は最新 1 件へ畳まれる(いまの値が要るなら getVar)。対象を絞るなら dlg.watchVars = ["a","b"]("*"=全部 / []=止める / void=既定)。
- named action の通知形:
bindings に組込以外の名前を書いたときの通知は、onAction(id, payload) の id が "<action>" 固定・付けた名前は payload(onAction("<action>", "prev_page"))。組込名(accept/cancel/nav_*/focus_*/page_*/scroll_*/none/passthrough)はセッション内で処理されホストへ来ない。id に自分の名前が来ると思って書くとその入力だけ黙って無反応になる(ホイール差し替えで踏みやすい)。
- フォーカス無しから方向キーで入る位置:
input.arrow_focus_enter = "first"(既定・収集順の先頭)/ "directional"(押した方向の端。右キーなら一番右)。initial_focus を置かない確認ダイアログ向け。
- 複数 ElementsDialog の ownership:
close() は自分のインスタンスだけ閉じる(IsHandlerActive(this) ゲート)。ブロッキング pump も自分の handler で終了判定。
6.5 読み上げ (スクリーンリーダー)
表示中の画面は OS のスクリーンリーダー (ナレーター / VoiceOver / Orca) から何もしなくても読まれる (名前は text / labeled_row.label / group.title 等から、役割は type から自動。入力欄は文字・単語・行単位)。使い方の全体は umbrella の doc/guide/Accessibility.md。
- 画面 JSON の
"a11y" キー (全ウィジェット共通): 文字列=名前の短縮形、辞書=label/label_id (名前) / description/description_id / role ("heading" 等で上書き) / value_var / live ("polite"/"assertive"、text_var の変化を読む) / hidden (子ごと外す)。トップレベル "a11y": {"title"|"title_id"} = 画面名。絵だけのボタン (sprite_button / atlas_*) は文字を持たないので必ず名前を付ける。TJS 辞書から渡すと true が 1 になるが、bool キーは数値も受ける。
- static (クラス全体):
announce(text[, assertive]) (任意の文を読ませる。AT 未接続でも受け付ける) / a11yActive (読み取り専用。Windows は窓が前面になるまで偽) / onA11yActiveChanged(active) (クラスへ関数を代入) / a11yMode ("auto"/"off") / a11yLabel (根の名前)。
- ゲーム本体 (Layer に描いた UI):
setGameA11y(nodes[, focus]) (ノード表を丸ごと渡す。%[id, role, name, value, states, rect=primary 座標, parent, num_*]) + onGameA11yAction(id, action, arg) (本体はフォーカスを動かさない → 状態を変えて setGameA11y を呼び直す) / clearGameA11y() / a11yLayers = true (フォーカス連鎖の Layer を自動で載せる。名前=hint、Layer に a11yName/a11yRole/a11yValue/a11yStates/a11yHidden/onA11yAction を生やして補う)。ゲームのノードはダイアログより下・モーダル中は隠れる。
- ⚠ どれも static なので場面を抜けるときに戻す (
clearGameA11y() / a11yLayers = false / onGameA11yAction = void)。対象はメインウィンドウだけ (showModalJson の専用窓・2 枚目以降の Window は未対応)。
- 実例:
data/a11y (選択肢を setGameA11y、ボタン類を a11yLayers、パネルの "a11y" キー)。検証は §8 の .a11y / .a11ylog / .a11ydo。
7. ハマりどころ (memory 由来・重要)
- DrawDevice 登録タイミング: 起動直後(初回フレーム前)は overlay renderer 未登録で
showJson が false を返すことがある。初回表示は Window.onContinuousHandler 等へ遅延し、成功するまでリトライする(data/demolib 参照)。[[reference_elements_dialog_drawdevice_timing]]
- GL DrawDevice 上で出ない: 提示中デバイスへ host 解決が追従していないと GL 画面上でパネルが出ない/操作不可。エンジン修正済だが、デモ側の「パネル再試行が自壊(shellClosePanel が pending を消す)」に注意。[[reference_elements_on_gl_drawdevice]]
- サイズ:
size は上限でfit-to-content。全画面化はできない(BeginScreen 実装のきめうち有り)。size の peek が widget "size" 属性に誤爆する既知点あり。[[reference_elements_overlay_dialog_sizing]]
- フォーカス奪取 / 共通ホットキー: ✅解決済(2026-08-11)。操作パネルは
showJson(json, true, false)+ホスト必須キーを ElementsDialog.registerHotKey で確保(§6)。旧回避策の grabFocus=false は表示専用 HUD 用。[[project_elements_global_shortcut]]
- サブクラスは
super.ElementsDialog() 必須。showFile は autopath 未対応な場面あり(相対解決に注意)。
- サブクラス内から static を触るときは
global.ElementsDialog.xxx。素の ElementsDialog は親クラス参照になり ElementsDialog.focusRing = false 等が「メンバが見つかりません」で落ちる。
- 入力/ナビ/部分再描画の切り分けには
-navlog(指定するだけで有効)。フォーカス移動・cursor-warp・パッド方向キーの到着を ms 付きで、100ms 超フレームの段別内訳(slow frame)、ラスタ 1 回ごとの「部分にできたか/できなかった理由」(raster partial= / no-partial:)が出る。計測画面=data/elements_audit(-audittest で自動巡回、-ignoremouse=yes を併用すると実マウスに邪魔されない)。
- 描画が重いと感じたらまず
ElementsDialog.renderStats の差分を見る(renderCount がアイドルで増えていないかも)。data/elements_bench に更新パターン別の計測画面がある。overlay 描画の支配項はテキストのラスタライズで、同内容のテキストは自動でビットマップキャッシュされる(毎フレーム内容が変わる HUD カウンタは意図的に載らない)。
- case-name 規約: 共有/公開リポ(krkrz_dev/elements)に案件固有名を書かない。elements リポのコメント等はホストを「SDL を使うホストアプリ」等の汎用表現で。[[feedback_elements_repo_project_agnostic]] [[feedback_no_case_names_in_shared_repo]]
8. 動作検証 (Agent / REPL)
GUI は krkrz-repl skill のファイルチャネル + Agent API で駆動・目視できる:
Agent.dialogs() — アクティブダイアログ配列(index/modal/active/screen/rect)。
Agent.dialogClick(i, id) — id 指定でボタン起動(座標不要・確実)。Agent.click(x,y) は座標。
Agent.text(str) — アクティブダイアログへテキスト入力(input_box)。
Agent.captureScreen(path) — overlay 込み実画面 PNG(次フレーム)。→ Read で目視。
- ドットコマンド:
.dlg / .dlgclose / .click X Y / .cap。
- 読み上げ:
.a11y (読み上げツリー JSON = Agent.a11yTree()) / .a11ylog [N] (おおよそ何と読むか) / .a11ydo <node> <action> [arg] (AT と同じ経路で操作 = Agent.a11yAction) / .say <text>。スクリーンリーダー無しで確かめられる。
- 検証時の注意: REPL eval は式評価。永続変数は
global.x = ...。多重 primary layer は不可(2枚目以降は new Layer(win, primary))。詳細は skill krkrz-repl。
9. 実装場所 (深掘り時)
- TJS
ElementsDialog バインド: common/visual/elements/DialogIntf.cpp(showJson/showDict/showFlow/startFlow/close/onAction/onClose/setVar/registerFont/active/defaultFontFamily/language/focusRing/renderCache/partialRedraw/renderScale/renderStats 等)。
- マネージャ:
common/visual/elements/(tTVPElementsDialogManager = z-order インスタンスリスト)。
- レンダラ提供口:
iTVPDialogRenderer/iTVPDialogRendererHost(tp_stub 公開)。SDL=SDLDialogRenderer / OGL=OGLDialogRenderer / WINVER=D3D11DialogRenderer / 独立窓=WinElementsModalRunner。
- JSON→UI: elements_modal(
json_layout.cpp / navigator / overlay_session)。プラグイン向け C ABI: tp_dialog_service.h。
- 関連 doc:
doc/Gamepad.md(pad↔key)、doc/D3D11Migration.md(DestRect)、external/elements/external/elements_modal/README.md。
関連 skill
tjs2(TJS 言語)/ krkrz(本体クラス API)/ krkrz-repl(REPL・Agent 駆動・captureScreen)。