8.19.34. Window size constraints

ImGui’s SetNextWindowSizeConstraints clamps the next window between a min and max corner size. The 2-arg form is enough for a static box constraint; the 3-arg form takes an ImGuiSizeCallback — a C function pointer ImGui invokes on every resize so the callback can re-shape the requested size (aspect-ratio lock, fixed-step quantization, “always square”, etc.).

The boost module imgui/imgui_window_constraints_builtin ships a daslang wrapper around the 3-arg form: an ImGuiSizeConstraints struct that wraps a daslang lambda + the existing C++ trampoline (SetNextWindowSizeConstraintsCallbackdas_invoke_lambda<void> in src/dasIMGUI.main.cpp). End-user code passes the wrapper:

// Module scope: ImGui keeps the pointer for the NEXT Begin(), so the
// wrapper must outlive the SetNextWindowSizeConstraints call.
let private ASPECT_RATIO = 16.0f / 9.0f

var private ASPECT_CN : ImGuiSizeConstraints <- ImGuiSizeConstraints(
    @ capture(= ASPECT_RATIO) (var data : ImGuiSizeCallbackData) : void {
        data.DesiredSize = float2(data.DesiredSize.x,
                                  data.DesiredSize.x / ASPECT_RATIO)
    })

SetNextWindowSizeConstraints(float2(0.0f, 0.0f), float2(FLT_MAX, FLT_MAX),
                             ASPECT_CN)

Seed the wrapper at its module-scope declaration and pass the same struct on every frame — the lambda carries its own capture state. A stack local will not do: auto-fit invokes the callback from inside Begin(), after the enclosing function has already returned.

The 2-arg SetNextWindowSizeConstraints(min, max) form stays on the boost-surface allow-list — only require this module when you need the callback form.

Source: modules/dasImgui/examples/tutorial/window_size_constraints.das.

8.19.34.1. Walkthrough

The recording drags each window by its bottom-right corner - the real “drag border” gesture - to a shape that violates its constraint, and the callback snaps it back: the first window, dragged to a wide-short rectangle, springs to a 400x400 square; the second, dragged tall, collapses to 480x270 (16:9); the third, dragged off-grid, lands on 350x200 (nearest 50). Each snapped size is asserted - a drag that didn’t resize, or a callback that didn’t reshape the request, aborts the recording.

  1options gen2
  2options _comment_hygiene = true
  3options gc
  4
  5require imgui
  6require imgui_app
  7require opengl/opengl_boost
  8require live/glfw_live
  9require live/live_api
 10require live/live_commands
 11require live/live_vars
 12require live_host
 13require imgui/imgui_live
 14require imgui/imgui_boost_runtime
 15require imgui/imgui_boost_v2
 16require imgui/imgui_widgets_builtin
 17require imgui/imgui_containers_builtin
 18require imgui/imgui_window_constraints_builtin
 19require imgui/imgui_visual_aids
 20require math
 21
 22// =============================================================================
 23// TUTORIAL: window_size_constraints — lambda-callback overload for
 24//           ImGui::SetNextWindowSizeConstraints.
 25//
 26// The 2-arg form `SetNextWindowSizeConstraints(min, max)` is plain-allowed
 27// at user-call sites — clamp the window between two corner sizes. The 3-arg
 28// form takes a `ImGuiSizeCallback`, a C function pointer ImGui invokes on
 29// every resize so the callback can re-shape the requested size (aspect-ratio
 30// lock, fixed step quantization, "always square", ...).
 31//
 32// The boost module `imgui/imgui_window_constraints_builtin` provides a
 33// daslang-side `ImGuiSizeConstraints` struct + ctor that wraps a daslang
 34// lambda; the matching 3-arg overload threads the wrapper through the
 35// existing C++ trampoline at `src/dasIMGUI.main.cpp:361-384`. End-user code
 36// can then write:
 37//
 38//   let aspect = @ capture(= ratio) (var data : ImGuiSizeCallbackData) : void {
 39//       data.DesiredSize.y = data.DesiredSize.x / ratio
 40//   }
 41//   SetNextWindowSizeConstraints(float2(0,0), float2(FLT_MAX, FLT_MAX),
 42//                                ImGuiSizeConstraints(aspect))
 43//
 44// This tutorial mounts three resizable windows side-by-side — square,
 45// aspect-16:9, fixed-step-50 — each with a body line printing its current
 46// (width, height) so resizing visibly snaps to the constraint.
 47//
 48// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/window_size_constraints.das
 49// LIVE:       daslang-live modules/dasImgui/examples/tutorial/window_size_constraints.das
 50// =============================================================================
 51
 52// Module-scope holders for the three ImGuiSizeConstraints. ImGui stores the
 53// callback pointer + user-data for use by the NEXT Begin(); the wrapper
 54// struct's address must outlive the SetNextWindowSizeConstraints call.
 55// Stack-locals don't survive long enough — auto-fit invokes the callback
 56// from inside Begin AFTER the wrapper function has returned. Module
 57// scope keeps the struct alive for the lifetime of the program.
 58// Each is seeded once, right here: daslang lambdas hold their own capture
 59// state, so a single per-shape struct serves every future Begin().
 60let private ASPECT_RATIO = 16.0f / 9.0f
 61let private FIXED_STEP = 50.0f
 62
 63var private SQUARE_CN : ImGuiSizeConstraints <- ImGuiSizeConstraints(
 64    @(var data : ImGuiSizeCallbackData) : void {
 65        let s = max(data.DesiredSize.x, data.DesiredSize.y)
 66        data.DesiredSize = float2(s, s)
 67    })
 68var private ASPECT_CN : ImGuiSizeConstraints <- ImGuiSizeConstraints(
 69    @ capture(= ASPECT_RATIO) (var data : ImGuiSizeCallbackData) : void {
 70        data.DesiredSize = float2(data.DesiredSize.x,
 71                                  float(int(data.DesiredSize.x / ASPECT_RATIO)))
 72    })
 73var private STEP_CN : ImGuiSizeConstraints <- ImGuiSizeConstraints(
 74    @ capture(= FIXED_STEP) (var data : ImGuiSizeCallbackData) : void {
 75        let rx = float(int(data.DesiredSize.x / FIXED_STEP + 0.5f)) * FIXED_STEP
 76        let ry = float(int(data.DesiredSize.y / FIXED_STEP + 0.5f)) * FIXED_STEP
 77        data.DesiredSize = float2(rx, ry)
 78    })
 79
 80[export]
 81def init() {
 82    live_create_window("dasImgui window_size_constraints tutorial", 980, 560)
 83    live_imgui_init(live_window)
 84    // Relies on FirstUseEver window sizes; disable imgui.ini so the documented
 85    // layout (and recording) is deterministic every run instead of restoring a
 86    // prior session. Tutorial-scoped on purpose — not a live_imgui_init change.
 87    DisableIniPersistence()
 88    let io & = unsafe(GetIO())
 89    GetStyle().FontScaleMain = 1.2
 90}
 91
 92[export]
 93def update() {
 94    if (!live_begin_frame()) return
 95    begin_frame()
 96
 97    ImGui_ImplGlfw_NewFrame()
 98    apply_synth_io_override()
 99    NewFrame()
100
101    // -------- 1. Square — max(w, h) wins both dims. --------
102    SetNextWindowPos(ImVec2(20.0f, 60.0f), ImGuiCond.FirstUseEver)
103    SetNextWindowSize(ImVec2(240.0f, 240.0f), ImGuiCond.FirstUseEver)
104    SetNextWindowSizeConstraints(float2(120.0f, 120.0f), float2(FLT_MAX, FLT_MAX),
105                                 SQUARE_CN)
106    window(SQUARE_WIN, (text = "Square", closable = false,
107                        flags = ImGuiWindowFlags.None)) {
108        let s = SQUARE_WIN.size
109        text(SQUARE_TEXT, (text = "Drag border - always square.\n"
110                                + "Size: {int(s.x)} x {int(s.y)}"))
111    }
112
113    // -------- 2. Aspect ratio 16:9 — height = width / aspect. --------
114    SetNextWindowPos(ImVec2(290.0f, 60.0f), ImGuiCond.FirstUseEver)
115    SetNextWindowSize(ImVec2(320.0f, 180.0f), ImGuiCond.FirstUseEver)
116    SetNextWindowSizeConstraints(float2(160.0f, 0.0f), float2(FLT_MAX, FLT_MAX),
117                                 ASPECT_CN)
118    window(ASPECT_WIN, (text = "Aspect 16:9", closable = false,
119                        flags = ImGuiWindowFlags.None)) {
120        let s = ASPECT_WIN.size
121        text(ASPECT_TEXT, (text = "Drag border - height tracks width / 16:9.\n"
122                                + "Size: {int(s.x)} x {int(s.y)}"))
123    }
124
125    // -------- 3. Fixed step 50 — both dims snap to nearest 50 px. --------
126    SetNextWindowPos(ImVec2(640.0f, 60.0f), ImGuiCond.FirstUseEver)
127    SetNextWindowSize(ImVec2(300.0f, 250.0f), ImGuiCond.FirstUseEver)
128    SetNextWindowSizeConstraints(float2(100.0f, 100.0f), float2(FLT_MAX, FLT_MAX),
129                                 STEP_CN)
130    window(STEP_WIN, (text = "Step 50", closable = false,
131                      flags = ImGuiWindowFlags.None)) {
132        let s = STEP_WIN.size
133        text(STEP_TEXT, (text = "Drag border - both dims snap to 50 px.\n"
134                              + "Size: {int(s.x)} x {int(s.y)}"))
135    }
136
137    end_of_frame()
138    Render()
139    var w, h : int
140    live_get_framebuffer_size(w, h)
141    glViewport(0, 0, w, h)
142    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
143    glClear(GL_COLOR_BUFFER_BIT)
144    live_imgui_render()
145
146    live_end_frame()
147}
148
149[export]
150def shutdown() {
151    live_imgui_shutdown()
152    live_destroy_window()
153}
154
155[export]
156def main() {
157    init()
158    while (!exit_requested()) {
159        update()
160        maybe_collect_gc()
161    }
162    shutdown()
163}

8.19.34.1.1. Requires

One extra module on top of the baseline boost layer:

  • imgui/imgui_window_constraints_builtin — the ImGuiSizeConstraints struct + ctor + 3-arg overload of SetNextWindowSizeConstraints.

8.19.34.1.2. The three callback patterns

The tutorial mounts three resizable windows demonstrating the canonical callback shapes:

Squaremax(w, h) wins both dims:

let sq <- @ (var data : ImGuiSizeCallbackData) : void {
    let s = max(data.DesiredSize.x, data.DesiredSize.y)
    data.DesiredSize = float2(s, s)
}

Aspect 16:9 — height tracks width / aspect_ratio:

let ratio = 16.0f / 9.0f
let aspect <- @ capture(= ratio) (var data : ImGuiSizeCallbackData) : void {
    data.DesiredSize = float2(data.DesiredSize.x,
                               data.DesiredSize.x / ratio)
}

Fixed step — both dims snap to the nearest multiple:

let step = 50.0f
let snap <- @ capture(= step) (var data : ImGuiSizeCallbackData) : void {
    let rx = float(int(data.DesiredSize.x / step + 0.5f)) * step
    let ry = float(int(data.DesiredSize.y / step + 0.5f)) * step
    data.DesiredSize = float2(rx, ry)
}

Capture rules follow the standard daslang lambda surface — capture(= var) for by-value, capture(& var) for by-reference. A plain @(...) with no capture clause is enough for callbacks that read only data.

8.19.34.1.3. Struct layout

The ImGuiSizeConstraints daslang struct mirrors das::DasImGuiSizeConstraints in src/dasIMGUI.main.cpp field-by-field:

struct ImGuiSizeConstraints {
    context : Context?
    callback : lambda<(var data : ImGuiSizeCallbackData) : void>
    at : LineInfo?
}

The C++ trampoline reads field offsets directly to dispatch the lambda through das_invoke_lambda<void>::invoke<ImGuiSizeCallbackData*>. Don’t re-order the fields — the binding is not symbolic.

8.19.34.1.4. Standalone vs live

Same convention as previous tutorials. Resize any of the three windows by dragging a border or corner — the callback fires on every drag delta and quantizes the result.

See also

Full source: modules/dasImgui/examples/tutorial/window_size_constraints.das

app_small ShowExampleAppConstrainedResize (mirrors ImGui’s imgui_demo.cpp:7885-7978) exercises 9 constraint options across both overload forms: examples/imgui_demo/app_small.das.

Integration test: modules/dasImgui/tests/test_app_small_constrained_resize.das.

Boost macros — the macro layer.