makepad-2.0-layout について
MakepadのUI開発者向けレイアウト資料です。CSS FlexboxやGridではなく、turtleによるサイズ決定と子要素の配置を解説します。スクロール領域、間隔、整列、ポップアップの配置も扱います。
主な機能
- 01Walkによるサイズ指定とLayout配置
- 02flow、間隔、パディング、整列
- 03スクロールコンテナの選択と設定
- 04ポップアップの絶対位置指定
- 05よくあるレイアウト問題の確認
活用シーン
- 01縦型ページや横型ツールバーの構築
- 02長いリストのスクロール領域設定
- 03トリガーウィジェットへのポップアップ固定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ zhanghandong/ makepad-skills --skill makepad-2.0-layoutインストール後、Claude Code で/makepad-2.0-layoutを入力して読み込まれたか確認します。
同梱ファイルreferences/layout-patterns.md
詳細
Makepad 2.0 レイアウトシステム
Makepad では レイアウトタートルシステムを使用します。CSS のフレックスボックスでも、CSS グリッドでもありません。タートルは子要素を一つずつたどり、次の2つの基本概念に従って各ウィジェットを配置します。
- Walk -- ウィジェット自身のサイズの決め方(幅、高さ、マージン)
- Layout -- コンテナが子要素を配置する方法(フロー、間隔、パディング、配置)
すべてのコンテナウィジェット(View、SolidView、RoundedView、ScrollYView など)には、Walk プロパティ(自身のサイズ)と Layout プロパティ(子要素のレイアウト方法)の両方があります。
Walk システム(ウィジェットのサイズ設定)
Walk は、個々のウィジェットが親の中でどれだけのスペースを確保するかを制御します。
width / height
| 構文 | 意味 |
|---|---|
width: Fill | 残りの水平方向のスペースをすべて埋めます(デフォルト) |
width: Fit | コンテンツに合わせて縮小します |
width: 200 | 200 ピクセルに固定します |
width: Fill{min: 100 max: 500} | 制約付きでスペースを埋めます |
width: Fit{max: Abs(300)} | コンテンツに合わせ、最大 300px に制限します |
height: Fill | 残りの垂直方向のスペースをすべて埋めます(デフォルト) |
height: Fit | コンテンツに合わせて縮小します |
height: 100 | 100 ピクセルに固定します |
use mod.prelude.widgets.*
// Fill: takes all available width
View{
width: Fill height: Fit
flow: Down
Label{text: "I stretch to fill the width"}
}
// Fit: shrinks to content
View{
width: Fit height: Fit
padding: 10
Label{text: "I am only as wide as this text"}
}
// Fixed: exact pixel size
View{
width: 300 height: 200
Label{text: "I am exactly 300x200 pixels"}
}
// Constrained Fill: fills but within bounds
View{
width: Fill{min: 200 max: 600} height: Fit
flow: Down padding: 16
Label{text: "I fill available space but stay between 200-600px"}
}
重要:コンテナでの height: Fit
これは Makepad で最もよくあるレイアウトの不具合です。
デフォルトの高さは Fill です。出力が Fit コンテナ内に描画される場合、Fit 内の Fill は循環依存を引き起こし、0 ピクセルとして解決されます。
その結果、UI 全体が見えなくなります。
ルール:親の高さが固定または Fill でない限り、すべての View、SolidView、RoundedView、および同様のコンテナに、必ず height: Fit を設定してください。
// CORRECT -- height: Fit makes the container visible
View{
width: Fill height: Fit
flow: Down padding: 10
Label{text: "I am visible"}
}
// WRONG -- defaults to height: Fill, resolves to 0px, invisible
View{
width: Fill
flow: Down padding: 10
Label{text: "I am invisible (0px tall)"}
}
height: Fill を使用してもよい例外:
- 高さが固定された親の中:
View{
height: 400
View{
height: Fill
Label{text: "I fill the 400px parent"}
}
}
- 最終的に既知のサイズ(例:Window の body)に到達する
height: Fillの連鎖内。 - ScrollYView はスクロールするために親全体を埋める必要があるので、常に
height: Fillを使用します。
margin
マージンはウィジェットの外側にスペースを追加します。
// Uniform margin on all sides
Label{text: "Hello" margin: 10}
// Selective margin with Inset
Label{
text: "Indented"
margin: Inset{top: 5 bottom: 5 left: 20 right: 20}
}
// Zero margin (note the trailing dot for float literal)
Label{text: "Flush" margin: 0.}
Layout システム(子要素の配置)
Layout は、コンテナが子要素をどのように配置するかを制御します。
flow(方向)
| 構文 | 意味 | CSS での相当表現 |
|---|---|---|
flow: Right | 左から右へ、一行に配置します(デフォルト) | flex-direction: row |
flow: Down | 上から下へ、一列に配置します | flex-direction: column |
flow: Overlay | 子要素を互いに重ねて配置します | position: absolute による重ね合わせ |
flow: Flow.Right{wrap: true} | 左から右へ、折り返して配置します | flex-wrap: wrap |
flow: Flow.Down{wrap: true} | 上から下へ、折り返して配置します | column wrap |
use mod.prelude.widgets.*
// Vertical stack (most common)
View{
width: Fill height: Fit
flow: Down spacing: 10
Label{text: "First"}
Label{text: "Second"}
Label{text: "Third"}
}
// Horizontal row
View{
width: Fill height: Fit
flow: Right spacing: 10
Label{text: "Left"}
Label{text: "Center"}
Label{text: "Right"}
}
// Overlay -- children stacked on top of each other
View{
width: Fill height: 200
flow: Overlay
Image{width: Fill height: Fill fit: ImageFit.Biggest}
View{
width: Fill height: Fit
align: Align{x: 0.5 y: 1.0}
padding: 10
Label{text: "Caption overlay" draw_text.color: #fff}
}
}
// Wrapping flow -- like a tag cloud or grid of cards
View{
width: Fill height: Fit
flow: Flow.Right{wrap: true}
spacing: 8
padding: 10
Label{text: "Tag 1" margin: 4}
Label{text: "Tag 2" margin: 4}
Label{text: "Tag 3" margin: 4}
Label{text: "Tag 4" margin: 4}
}
spacing
子要素間の間隔です。単一の数値を指定すると、均一に適用されます。
View{
flow: Down spacing: 12
Label{text: "12px gap below me"}
Label{text: "12px gap above and below me"}
Label{text: "12px gap above me"}
}
padding
コンテナの端と子要素の間の内側のスペースです。
// Uniform padding
View{
width: Fill height: Fit
padding: 20
Label{text: "20px padding on all sides"}
}
// Selective padding with Inset
View{
width: Fill height: Fit
padding: Inset{top: 10 bottom: 10 left: 24 right: 24}
Label{text: "Different padding per side"}
}
align(子要素の配置)
配置は、コンテナ内の残りのスペースに対する子要素の位置を決めます。 各軸の値は 0.0(始点)から 1.0(終点)の範囲です。
配置の参照表
| 短縮形 | 相当表現 | 説明 |
|---|---|---|
Center | Align{x: 0.5 y: 0.5} | 両軸の中央 |
HCenter | Align{x: 0.5 y: 0.0} | 水平方向の中央、上揃え |
VCenter | Align{x: 0.0 y: 0.5} | 左揃え、垂直方向の中央 |
TopLeft | Align{x: 0.0 y: 0.0} | 左上隅(デフォルト) |
Align{x: 1.0 y: 0.0} | -- | 右上隅 |
Align{x: 0.0 y: 1.0} | -- | 左下隅 |
Align{x: 1.0 y: 1.0} | -- | 右下隅 |
Align{x: 0.5 y: 1.0} | -- | 下中央 |
use mod.prelude.widgets.*
// Center everything
View{
width: Fill height: 300
align: Center
Label{text: "I am centered"}
}
// Horizontal center only (children flow from top)
View{
width: Fill height: Fit
flow: Down
align: HCenter
Label{text: "I am horizontally centered"}
}
// Vertically center children in a horizontal row
View{
width: Fill height: 60
flow: Right spacing: 10
align: Align{y: 0.5}
Label{text: "Vertically centered" draw_text.text_style.font_size: 14}
Label{text: "Small text" draw_text.text_style.font_size: 9}
}
clip_x / clip_y
コンテンツがはみ出したときにクリップするかどうかを制御します。
// Clip overflow (default behavior)
View{
width: 200 height: 100
clip_x: true
clip_y: true
Label{text: "Very long text that will be clipped at the container boundary"}
}
// Allow overflow to be visible
View{
width: 200 height: 100
clip_x: false
clip_y: false
Label{text: "This text can overflow beyond the container"}
}
重要な境界: clip_x: false / clip_y: false にすると、ローカルの子要素を親の外側に描画できるだけです。その子要素がウィンドウレベルの真のオーバーレイになるわけではありません。UI 要素が独立して浮かぶポップアップ/メニュー/ツールチップの場合は、ローカルなオーバーフローに頼らず、トップレベルの Modal/オーバーレイの所有者を使用してください。
オーバーレイポップアップ:walk.abs_pos と margin の使い分け
オーバーレイ(Modal、ツールチップレイヤー、ポップアップの親要素)内でポップアップ形式の位置指定を行う場合、実行時に margin を調整するよりも walk.abs_pos を使用してください。
marginはレイアウト上の余白です。通常のフローにある子要素の位置を微調整するのに適しています。walk.abs_posは、オーバーレイ形式の配置に使う明示的なタートルアンカーです。button.area().clipped_rect(cx)を使うと、view_shiftとクリッピングを含む、トリガーの実際のスクリーン座標上の矩形を取得できます。- オーバーレイのコンテンツでは、ポップアップの絶対スクリーン座標上の位置を計算し、それを
popup.walk.abs_pos = Some(dvec2(x, y))に設定します。
let button_rect = button.area().clipped_rect(cx);
let popup_pos = dvec2(button_rect.pos.x, button_rect.pos.y - 294.0);
if let Some(mut popup) = self.view(cx, ids!(popup)).borrow_mut() {
popup.walk.abs_pos = Some(popup_pos);
}
目安:
- 通常のレイアウトツリー内のポップアップで、わずかなはみ出しのみ必要な場合:ローカルの子要素+
clip_x/clip_y: false - ボタンを基準に配置し、コンポーネントの外側に視覚的に表示するポップアップの場合:トップレベルのオーバーレイ+
walk.abs_pos
よくある間違い: script_apply_eval! を使ってオーバーレイのコンテンツに margin.top/margin.left を設定し、安定したポップアップ座標が得られると期待することです。ポップアップを明示的にアンカーで固定するのではなく、レイアウトの調整に委ねているため、誤解を招く結果になりがちです。
Inset の構文
Inset 型は padding と margin の両方に使われます。次の 2 つの形式に対応しています。
// Bare number -- uniform on all four sides
padding: 10
margin: 5
// Inset struct -- specify individual sides
padding: Inset{top: 10 bottom: 10 left: 20 right: 20}
margin: Inset{top: 0 bottom: 8 left: 0 right: 0}
// Zero (use trailing dot for float literal)
margin: 0.
// You can omit sides you do not need -- they default to 0
padding: Inset{left: 16 right: 16}
padding と margin はどちらも同じ Inset 構文を受け付けます。Padding はコンテナの内側、margin は外側の余白です。
スクロール可能なコンテナ
Makepad には、スクロール可能なビューが 3 種類用意されています。すべての View プロパティを継承し、スクロールバーの動作を追加します。
| ウィジェット | スクロール方向 | 主な用途 |
|---|---|---|
ScrollYView | 垂直のみ | 長いリスト、ページコンテンツ |
ScrollXView | 水平のみ | 幅の広いテーブル、タイムライン |
ScrollXYView | 両方向 | マップ、キャンバス、大きなコンテンツ |
use mod.prelude.widgets.*
// Vertical scrolling -- the most common pattern
// Note: ScrollYView uses height: Fill (not Fit) to define the scroll viewport
ScrollYView{
width: Fill height: Fill
flow: Down padding: 10 spacing: 8
Label{text: "Item 1"}
Label{text: "Item 2"}
Label{text: "Item 3"}
Label{text: "Item 4"}
Label{text: "Item 5"}
Label{text: "Item 6"}
}
// Horizontal scrolling
ScrollXView{
width: Fill height: 60
flow: Right spacing: 10 padding: 10
align: Align{y: 0.5}
Label{text: "Tab 1"}
Label{text: "Tab 2"}
Label{text: "Tab 3"}
Label{text: "Tab 4"}
}
// Both-axis scrolling
ScrollXYView{
width: Fill height: Fill
Label{text: "Large content that can be scrolled in both directions"}
}
使い分け:
ScrollYView-- ページ本文、リスト、垂直方向のコンテンツ。90% のケースではこれを使います。ScrollXView-- 水平タブバー、コードのスクロール、タイムラインビュー。ScrollXYView-- 2D キャンバス、マップ、スプレッドシート形式のコンテンツ。
重要: スクロール可能なビューでは、height: Fit ではなく height: Fill を使います。スクロールするための固定ビューポートが必要だからです。内部のコンテンツはビューポートを超えて広がります。
Filler(スペーサーウィジェット)
Filler{} は View{width: Fill height: Fill} と同等です。隣接する要素を離して配置します。
重要なルール:Filler は width: Fit の兄弟要素の間でのみ使用してください。
width: Fill の兄弟要素の隣では Filler{} を使用しないでください。両者が残りのスペースを奪い合い、50/50 に分割されてテキストがクリッピングされます。
use mod.prelude.widgets.*
// CORRECT: Filler between Fit siblings
View{
width: Fill height: Fit
flow: Right
align: Align{y: 0.5}
Label{text: "Left side"}
Filler{}
Label{text: "Right side"}
}
// WRONG: Filler next to a Fill sibling -- text gets clipped
View{
width: Fill height: Fit
flow: Right
Label{width: Fill text: "This gets clipped to half width"}
Filler{}
Label{text: "Tag"}
}
// CORRECT alternative: width: Fill naturally pushes Fit siblings
View{
width: Fill height: Fit
flow: Right
View{
width: Fill height: Fit
flow: Down
Label{text: "Title takes remaining space"}
Label{text: "Subtitle"}
}
Label{text: "Tag"}
}
よくあるレイアウトパターン
垂直ページレイアウト
use mod.prelude.widgets.*
View{
width: Fill height: Fit
flow: Down spacing: 16 padding: 20
Label{text: "Page Title" draw_text.color: #fff draw_text.text_style.font_size: 18}
Label{text: "Subtitle text" draw_text.color: #aaa draw_text.text_style.font_size: 12}
Hr{}
Label{text: "Body content goes here" draw_text.color: #ddd}
}
水平ツールバー
use mod.prelude.widgets.*
SolidView{
width: Fill height: 44
flow: Right spacing: 8
padding: Inset{left: 12 right: 12}
align: Align{y: 0.5}
draw_bg.color: #2a2a3d
ButtonFlatter{text: "File"}
ButtonFlatter{text: "Edit"}
ButtonFlatter{text: "View"}
Filler{}
ButtonFlat{text: "Run"}
}
折り返しのあるカードグリッド
use mod.prelude.widgets.*
let Card = RoundedView{
width: 180 height: Fit
padding: 12 flow: Down spacing: 6
draw_bg.color: #334
draw_bg.border_radius: 8.0
title := Label{text: "Card" draw_text.color: #fff draw_text.text_style.font_size: 12}
body := Label{text: "Content" draw_text.color: #aaa draw_text.text_style.font_size: 10}
}
View{
width: Fill height: Fit
flow: Flow.Right{wrap: true}
spacing: 10 padding: 16
Card{title.text: "Design" body.text: "UI mockups"}
Card{title.text: "Backend" body.text: "API endpoints"}
Card{title.text: "Testing" body.text: "Unit tests"}
Card{title.text: "Deploy" body.text: "CI/CD pipeline"}
}
コンテンツの中央配置
use mod.prelude.widgets.*
View{
width: Fill height: 400
align: Center
flow: Down spacing: 12
Label{text: "Welcome" draw_text.color: #fff draw_text.text_style.font_size: 24}
Label{text: "Click below to get started" draw_text.color: #aaa}
Button{text: "Get Started"}
}
分割パネル(サイドバー+コンテンツ)
use mod.prelude.widgets.*
// Simple approach with fixed sidebar
View{
width: Fill height: Fill
flow: Right
SolidView{
width: 250 height: Fill
draw_bg.color: #1a1a2e
flow: Down padding: 12 spacing: 8
Label{text: "Navigation" draw_text.color: #fff draw_text.text_style.font_size: 14}
Label{text: "Home" draw_text.color: #aaa}
Label{text: "Settings" draw_text.color: #aaa}
Label{text: "About" draw_text.color: #aaa}
}
View{
width: Fill height: Fill
flow: Down padding: 20 spacing: 10
Label{text: "Main Content" draw_text.color: #fff draw_text.text_style.font_size: 16}
Label{text: "Page body here" draw_text.color: #ddd}
}
}
// Resizable approach with Splitter
Splitter{
axis: SplitterAxis.Horizontal
align: SplitterAlign.FromA(250.0)
a := sidebar
b := main_content
}
sidebar := SolidView{
width: Fill height: Fill
draw_bg.color: #1a1a2e
flow: Down padding: 12
Label{text: "Sidebar" draw_text.color: #fff}
}
main_content := View{
width: Fill height: Fill
flow: Down padding: 20
Label{text: "Content" draw_text.color: #fff}
}
固定ヘッダー+スクロール可能な本文+固定フッター
use mod.prelude.widgets.*
View{
width: Fill height: Fill
flow: Down
// Fixed header
SolidView{
width: Fill height: Fit
padding: Inset{top: 12 bottom: 12 left: 20 right: 20}
draw_bg.color: #2a2a3d
flow: Right
align: Align{y: 0.5}
Label{text: "App Title" draw_text.color: #fff draw_text.text_style.font_size: 16}
Filler{}
ButtonFlatter{text: "Settings"}
}
// Scrollable body (height: Fill takes remaining space)
ScrollYView{
width: Fill height: Fill
flow: Down padding: 16 spacing: 10
new_batch: true
Label{text: "Scrollable content item 1" draw_text.color: #ddd}
Label{text: "Scrollable content item 2" draw_text.color: #ddd}
Label{text: "Scrollable content item 3" draw_text.color: #ddd}
Label{text: "Scrollable content item 4" draw_text.color: #ddd}
Label{text: "Scrollable content item 5" draw_text.color: #ddd}
}
// Fixed footer
SolidView{
width: Fill height: Fit
padding: Inset{top: 8 bottom: 8 left: 20 right: 20}
draw_bg.color: #1e1e2e
flow: Right
align: Align{y: 0.5}
Label{text: "Status: Ready" draw_text.color: #888 draw_text.text_style.font_size: 10}
Filler{}
Label{text: "v1.0" draw_text.color: #666 draw_text.text_style.font_size: 10}
}
}
オーバーレイ/モーダルの配置
use mod.prelude.widgets.*
View{
width: Fill height: 400
flow: Overlay
// Base layer -- the page content
View{
width: Fill height: Fill
flow: Down padding: 20
Label{text: "Background page content" draw_text.color: #888}
}
// Overlay layer -- centered modal dialog
View{
width: Fill height: Fill
align: Center
RoundedView{
width: 320 height: Fit
padding: 20 flow: Down spacing: 12
draw_bg.color: #2a2a3d
draw_bg.border_radius: 12.0
new_batch: true
Label{text: "Confirm Action" draw_text.color: #fff draw_text.text_style.font_size: 16}
Label{text: "Are you sure you want to proceed?" draw_text.color: #aaa}
View{
width: Fill height: Fit
flow: Right spacing: 8
align: Align{x: 1.0}
ButtonFlat{text: "Cancel"}
Button{text: "Confirm"}
}
}
}
}
重要なルールのまとめ
1. すべてのコンテナで height: Fit を指定する(最も多いバグ)
固定高さまたは Fill 高さの親要素チェーン内にある場合を除き、すべての View、SolidView、RoundedView に height: Fit を指定してください。指定を忘れると UI が見えなくなります(0px)。
2. ルートコンテナには width: Fill を指定する
最外部のコンテナに固定ピクセル幅を使わないでください。利用可能なスペースに合わせて調整されません。ルート要素には必ず width: Fill を指定してください。
3. View に show_bg とテキストの子要素がある場合は new_batch: true を指定する
コンテナで show_bg: true を設定している場合(SolidView、RoundedView なども含む)に Labels などのテキストを含むなら、コンテナに new_batch: true を設定してください。設定しないと、GPU の描画コールのバッチ処理により、テキストが背景の後ろに描画されることがあります。
// CORRECT: new_batch ensures text draws on top of background
RoundedView{
width: Fill height: Fit
padding: 12 flow: Down
draw_bg.color: #334
draw_bg.border_radius: 8.0
new_batch: true
Label{text: "Visible text" draw_text.color: #fff}
}
4. width: Fill の兄弟要素の隣に Filler を置かない
Filler と width: Fill の兄弟要素は同じ残りのスペースを奪い合い、50/50 に分割されてテキストがクリッピングされます。Filler は width: Fit の兄弟要素の間でのみ使用してください。
5. ScrollYView は height: Fit ではなく height: Fill を使用します
スクロール可能なビューには、固定されたビューポートが必要です。ScrollYView に height: Fill を指定して親いっぱいに広げ、その領域内でコンテンツをスクロールさせます。
ドキュメント
- レイアウトパターンの例と完全なコード:
./references/layout-patterns.md - Splash 言語マニュアル:
/splash.md - ウィジェットカタログ:
/skills/makepad-2.0-widgets/references/widget-catalog.md