imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Make full-viewport imgui window move host platform window (was: borderless imgui window)

Open Tellegar opened this issue 1 year ago • 7 comments

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;
}

Tellegar avatar Dec 29 '24 00:12 Tellegar

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.

PathogenDavid avatar Dec 29 '24 23:12 PathogenDavid

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.

o-3-o avatar Dec 30 '24 06:12 o-3-o

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.

ocornut avatar Dec 30 '24 09:12 ocornut

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.

WerWolv avatar Dec 31 '24 22:12 WerWolv

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. ^_^

o-3-o avatar Feb 15 '25 04:02 o-3-o

@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.

ocornut avatar Feb 18 '25 17:02 ocornut