mcpField
M
makepad-2.0-layout

makepad-2.0-layout

github.com/zhanghandongデザイン・UI75253 回インストール29 週間前に更新
リポジトリ

makepad-2.0-layout について

MakepadのUI開発者向けレイアウト資料です。CSS FlexboxやGridではなく、turtleによるサイズ決定と子要素の配置を解説します。スクロール領域、間隔、整列、ポップアップの配置も扱います。

主な機能

  1. 01Walkによるサイズ指定とLayout配置
  2. 02flow、間隔、パディング、整列
  3. 03スクロールコンテナの選択と設定
  4. 04ポップアップの絶対位置指定
  5. 05よくあるレイアウト問題の確認

活用シーン

  1. 01縦型ページや横型ツールバーの構築
  2. 02長いリストのスクロール領域設定
  3. 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: 200200 ピクセルに固定します
width: Fill{min: 100 max: 500}制約付きでスペースを埋めます
width: Fit{max: Abs(300)}コンテンツに合わせ、最大 300px に制限します
height: Fill残りの垂直方向のスペースをすべて埋めます(デフォルト)
height: Fitコンテンツに合わせて縮小します
height: 100100 ピクセルに固定します
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 を使用してもよい例外:

  1. 高さが固定された親の中:
View{
    height: 400
    View{
        height: Fill
        Label{text: "I fill the 400px parent"}
    }
}
  1. 最終的に既知のサイズ(例:Window の body)に到達する height: Fill の連鎖内。
  2. 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(終点)の範囲です。

配置の参照表
短縮形相当表現説明
CenterAlign{x: 0.5 y: 0.5}両軸の中央
HCenterAlign{x: 0.5 y: 0.0}水平方向の中央、上揃え
VCenterAlign{x: 0.0 y: 0.5}左揃え、垂直方向の中央
TopLeftAlign{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

コメント