Make full-viewport imgui window move host platform window (was: borderless imgui window)
Version/Branch of Dear ImGui:
both latest master and latest docking
Back-ends:
imgui_impl_sdl2.cpp + imgui_impl_sdlrenderer2.cpp
Compiler, OS:
windows 11 + msys clang
Full config/build information:
Dear ImGui 1.91.7 WIP (19163)
--------------------------------
sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=202302
define: _WIN32
define: _WIN64
define: __MINGW32__
define: __MINGW64__
define: __GNUC__=4
define: __clang_version__=18.1.8
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
--------------------------------
io.BackendPlatformName: imgui_impl_sdl2
io.BackendRendererName: imgui_impl_sdlrenderer2
io.ConfigFlags: 0x00000000
io.ConfigViewportsNoDecoration
io.ConfigNavCaptureKeyboard
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00000C0E
HasMouseCursors
HasSetMousePos
PlatformHasViewports
HasMouseHoveredViewport
RendererHasVtxOffset
--------------------------------
io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 512,128
io.DisplaySize: 1029.00,688.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------
style.WindowPadding: 8.00,8.00
style.WindowBorderSize: 1.00
style.FramePadding: 4.00,3.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 8.00,4.00
style.ItemInnerSpacing: 4.00,4.00
Details:
My Issue/Question:
is it possible to make the imgui inherit the platform window? e.g. i create a sdl borderless window and i would like to have a single imgui window to draw its own titlebar, handle resize/movement of the underlying platform window
i can achieve this (but with tearing and other issues):
ImGui::SetNextWindowPos({0, 0}, ImGuiCond_Once);
ImGui::SetNextWindowSize(
ImVec2(static_cast<float>(screen.width), static_cast<float>(screen.height)),
ImGuiCond_Once
);
ImGui::Begin(
"hello world",
&is_running,
ImGuiWindowFlags_NoBringToFrontOnFocus
);
auto pos = ImGui::GetWindowPos();
auto size = ImGui::GetWindowSize();
if (pos.x != 0 or pos.y != 0) {
ImGui::SetWindowPos({0, 0});
SDL_SetWindowPosition(window, sdl_pos.x + static_cast<int>(pos.x), sdl_pos.y + static_cast<int>(pos.y));
}
if (static_cast<int>(size.x) != screen.width or static_cast<int>(size.y) != screen.height) {
SDL_SetWindowSize(window, static_cast<int>(size.x), static_cast<int>(size.y));
}
// window contents ...
ImGui::End();
i think the tearing comes from using ImGui::SetWindowPos({0, 0}); which updates the position in the next frame, not immediatelly, but using ImGui::SetNextWindowPos would throw away the information about the window repositioning.
otherwise i would have to implement the window repositioning manually via sdl event handling.
docking branch supports this on some renderer backends (not sdlrender2), but i cant figure out how to have main window to be the imgui window
Screenshots/Video:
https://github.com/user-attachments/assets/d17ad5bc-28d7-49f3-b06f-e98d6e6332a5
Minimal, Complete and Verifiable Example code:
#define SDL_MAIN_HANDLED
#include <chrono>
#include <imgui.h>
#include <imgui_impl_sdl2.h>
#include <imgui_impl_sdlrenderer2.h>
#include <thread>
#include <SDL2/SDL.h>
using namespace std::chrono;
using namespace std::chrono_literals;
constexpr double fps_limit = 60;
constexpr auto frame_time = duration_cast<steady_clock::duration>(1s / fps_limit);
template<typename T, auto create, auto destroy>
struct PtrHolder {
T data{};
PtrHolder() = default;
explicit PtrHolder(T *ptr) : data(ptr) {}
PtrHolder(auto &&... args) {
data = create(std::forward<decltype(args)>(args)...);
}
~PtrHolder() {
destroy(data);
}
operator auto() {
return data;
}
operator bool() {
return data != nullptr;
}
};
struct Screen {
int width, height;
} screen = {500, 500};
using sdlWindow = PtrHolder<SDL_Window*, SDL_CreateWindow, SDL_DestroyWindow>;
using sdlRenderer = PtrHolder<SDL_Renderer*, SDL_CreateRenderer, SDL_DestroyRenderer>;
void handle_events(bool &is_running, SDL_Window *window) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
is_running = false;
if (event.type == SDL_WINDOWEVENT and
event.window.event == SDL_WINDOWEVENT_CLOSE and
event.window.windowID == SDL_GetWindowID(window))
is_running = false;
}
}
int main() {
SDL_Init(SDL_INIT_VIDEO);
bool is_borderless = true;
sdlWindow window(
"borderless_window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
screen.width,
screen.height,
SDL_WINDOW_RESIZABLE |
(is_borderless
? SDL_WINDOW_BORDERLESS
: 0)
);
if (not window) return -1;
sdlRenderer renderer(
window,
-1,
SDL_RENDERER_ACCELERATED
);
if (not renderer) return -1;
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui_ImplSDL2_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer2_Init(renderer);
ImGuiIO &io = ImGui::GetIO();
io.IniFilename = nullptr;
auto frame_start = steady_clock::now();
bool is_running = true;
while (is_running) {
handle_events(is_running, window);
struct {
int x = 0, y = 0;
} sdl_pos;
SDL_GetWindowPosition(window, &sdl_pos.x, &sdl_pos.y);
ImGui_ImplSDLRenderer2_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowPos({0, 0}, ImGuiCond_Once);
ImGui::SetNextWindowSize(
ImVec2(static_cast<float>(screen.width), static_cast<float>(screen.height)),
ImGuiCond_Once
);
ImGui::Begin(
"hello world",
&is_running,
ImGuiWindowFlags_NoBringToFrontOnFocus
);
auto pos = ImGui::GetWindowPos();
auto size = ImGui::GetWindowSize();
if (pos.x != 0 or pos.y != 0) {
ImGui::SetWindowPos({0, 0});
SDL_SetWindowPosition(window, sdl_pos.x + static_cast<int>(pos.x), sdl_pos.y + static_cast<int>(pos.y));
}
if (static_cast<int>(size.x) != screen.width or static_cast<int>(size.y) != screen.height) {
SDL_SetWindowSize(window, static_cast<int>(size.x), static_cast<int>(size.y));
}
ImGui::Text("sdl_pos: %d %d", sdl_pos.x, sdl_pos.y);
ImGui::Text("pos: %f %f", pos.x, pos.y);
ImGui::Text("size: %f %f", size.x, size.y);
ImGui::End();
static bool show_demo_window = false;
if (ImGui::IsKeyPressed(ImGuiKey_F12))
show_demo_window = not show_demo_window;
if (show_demo_window)
ImGui::ShowDemoWindow(&show_demo_window);
if (ImGui::IsKeyPressed(ImGuiKey_F11))
SDL_SetWindowBordered(window, static_cast<SDL_bool>(not((is_borderless = not is_borderless))));
ImGui::Render();
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), renderer);
SDL_RenderPresent(renderer);
auto now = steady_clock::now();
if (now - frame_start <= frame_time) {
std::this_thread::sleep_until(frame_start + frame_time);
frame_start += frame_time;
} else // missed frame
frame_start = now;
}
SDL_Quit();
return 0;
}
There's currently no straightforward way to accomplish this. Proper support is covered by https://github.com/ocornut/imgui/issues/3680 and https://github.com/ocornut/imgui/issues/3350
ImHex appears to do what you're wanting. I've never dug into how they're doing it, but it might be worth looking into.
otherwise i would have to implement the window repositioning manually via sdl event handling.
This is basically how things work when multi-viewports are in use.
Yes, this feature is still necessary for developing applications with ImGui. I don't know if this is in the plan. I have also been trying to deal with this problem, but it is not ideal because I also want to adapt to APIs such as ImGui::SetNextWindowSizeConstraints.
It’s a frequently requested feature to be able to start a window drag from any desired location, so I will eventually work on it.
It may be implemented in two ways: emulating move (the same way we currently do when dragging eg a viewport, or forwarding to the OS move which is more involved but has a few advantages.
ImHex does very platform-dependent things to handle this correctly.
Instead of having ImGui inherit the window in some way, we do one of the following:
- On Windows we use a borderless window but then define a custom windowProc function so that Windows Aero, resizing and window decoration such as the shadow drawn around the window works correctly. This works great for the most part but on some Intel HD GPUs, this causes the window content to be shifted weirdly
- Source: https://github.com/WerWolv/ImHex/blob/4c153dc76de65cfab3fea992edcf1fb7c30b3ef9/main/gui/source/window/win_window.cpp#L150-L313
- On macOS, we basically just tell the OS to turn the title bar background invisible and move it down into the client area. That way the OS handles basically everything for you and you can just draw where the title bar used to be.
- Source: https://github.com/WerWolv/ImHex/blob/bb594a459f3d90bc2e01d897ef5e3618be205393/lib/libimhex/source/helpers/utils_macos.m#L69-L74
- On Linux it gets a bit tricky. ImHex doesn't implement any of this right now but you would need to create a borderless window and then talk to libdecor to handle the window decoration minus the title bar
- I believe this is actually the only way to do it properly because Wayland, by design, doesn't allow applications to move or resize their window.
It’s a frequently requested feature to be able to start a window drag from any desired location, so I will eventually work on it.
It may be implemented in two ways: emulating move (the same way we currently do when dragging eg a viewport, or forwarding to the OS move which is more involved but has a few advantages.
Hello ocornut , when will this plan be implemented? I have been looking forward to it for several versions. ^_^
@lailoken pointed out to me that hiding the main viewport mostly works and is trivial:
// Hide the main window
::ShowWindow(hwnd, SW_HIDE);
io.ConfigViewportsNoDefaultParent = true; // make child viewports main viewports... no auto close
io.ConfigViewportsNoAutoMerge = true;
io.ConfigViewportsNoDecoration = false; // we want OS decorations
What happens there is that imgui will still see this viewport but it really won't be used for anything.
It only "mostly works" because some code may rely on using ID or position from GetMainViewport(), namely:
- it's used for CTRL+Tab overlay.
- it's used by
BeginMainMenuBar() - it's used as a fallback here and there.
Everything else seems overridable or bypassable. So this already works 95% I would say.