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 (das_invoke_lambda<void> at src/dasIMGUI.main.cpp:361). End-user code passes the wrapper directly:

let aspect = @ capture(= ratio) (var data : ImGuiSizeCallbackData) : void {
    data.DesiredSize = float2(data.DesiredSize.x,
                               data.DesiredSize.x / ratio)
}
SetNextWindowSizeConstraints(float2(0,0), float2(FLT_MAX, FLT_MAX),
                             ImGuiSizeConstraints(aspect))

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: examples/tutorial/window_size_constraints.das.

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
  2
  3require imgui
  4require imgui_app
  5require opengl/opengl_boost
  6require live/glfw_live
  7require live/live_api
  8require live/live_commands
  9require live/live_vars
 10require live_host
 11require imgui/imgui_live
 12require imgui/imgui_boost_runtime
 13require imgui/imgui_boost_v2
 14require imgui/imgui_widgets_builtin
 15require imgui/imgui_containers_builtin
 16require imgui/imgui_window_constraints_builtin
 17require imgui/imgui_visual_aids
 18require math
 19
 20// =============================================================================
 21// TUTORIAL: window_size_constraints — lambda-callback overload for
 22//           ImGui::SetNextWindowSizeConstraints.
 23//
 24// The 2-arg form `SetNextWindowSizeConstraints(min, max)` is plain-allowed
 25// at user-call sites — clamp the window between two corner sizes. The 3-arg
 26// form takes a `ImGuiSizeCallback`, a C function pointer ImGui invokes on
 27// every resize so the callback can re-shape the requested size (aspect-ratio
 28// lock, fixed step quantization, "always square", ...).
 29//
 30// The boost module `imgui/imgui_window_constraints_builtin` provides a
 31// daslang-side `ImGuiSizeConstraints` struct + ctor that wraps a daslang
 32// lambda; the matching 3-arg overload threads the wrapper through the
 33// existing C++ trampoline at `src/dasIMGUI.main.cpp:361-384`. End-user code
 34// can then write:
 35//
 36//   let aspect = @ capture(= ratio) (var data : ImGuiSizeCallbackData) : void {
 37//       data.DesiredSize.y = data.DesiredSize.x / ratio
 38//   }
 39//   SetNextWindowSizeConstraints(float2(0,0), float2(FLT_MAX, FLT_MAX),
 40//                                ImGuiSizeConstraints(aspect))
 41//
 42// This tutorial mounts three resizable windows side-by-side — square,
 43// aspect-16:9, fixed-step-50 — each with a body line printing its current
 44// (width, height) so resizing visibly snaps to the constraint.
 45//
 46// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/window_size_constraints.das
 47// LIVE:       daslang-live modules/dasImgui/examples/tutorial/window_size_constraints.das
 48// =============================================================================
 49
 50// Module-scope holders for the three ImGuiSizeConstraints. ImGui stores the
 51// callback pointer + user-data for use by the NEXT Begin(); the wrapper
 52// struct's address must outlive the SetNextWindowSizeConstraints call.
 53// Stack-locals don't survive long enough — auto-fit invokes the callback
 54// from inside Begin AFTER the wrapper function has returned. Module
 55// scope keeps the struct alive for the lifetime of the program.
 56var private SQUARE_CN : ImGuiSizeConstraints
 57var private ASPECT_CN : ImGuiSizeConstraints
 58var private STEP_CN : ImGuiSizeConstraints
 59
 60[export]
 61def init() {
 62    live_create_window("dasImgui window_size_constraints tutorial", 980, 560)
 63    live_imgui_init(live_window)
 64    // This tutorial relies on FirstUseEver window sizes; disable imgui.ini so the
 65    // documented layout (and the recording) is deterministic every run instead of
 66    // restoring whatever a prior session left. Tutorial-scoped on purpose — not a
 67    // blanket change to live_imgui_init.
 68    DisableIniPersistence()
 69    let io & = unsafe(GetIO())
 70    GetStyle().FontScaleMain = 1.2
 71
 72    // Seed the three constraints once at init — daslang lambdas hold their
 73    // own capture state, so a single per-shape struct is enough for all
 74    // future Begin() invocations of the matching window.
 75    SQUARE_CN <- ImGuiSizeConstraints(@(var data : ImGuiSizeCallbackData) : void {
 76        let s = max(data.DesiredSize.x, data.DesiredSize.y)
 77        data.DesiredSize = float2(s, s)
 78    })
 79    let ratio = 16.0f / 9.0f
 80    ASPECT_CN <- ImGuiSizeConstraints(@ capture(= ratio) (var data : ImGuiSizeCallbackData) : void {
 81        data.DesiredSize = float2(data.DesiredSize.x,
 82                                  float(int(data.DesiredSize.x / ratio)))
 83    })
 84    let step = 50.0f
 85    STEP_CN <- ImGuiSizeConstraints(@ capture(= step) (var data : ImGuiSizeCallbackData) : void {
 86        let rx = float(int(data.DesiredSize.x / step + 0.5f)) * step
 87        let ry = float(int(data.DesiredSize.y / step + 0.5f)) * step
 88        data.DesiredSize = float2(rx, ry)
 89    })
 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    }
161    shutdown()
162}

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.

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. The @ (no-capture) form works for callbacks that read only data.

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.

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: 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: tests/integration/test_app_small_constrained_resize.das.

Boost macros — the macro layer.