21274 lines
1.0 MiB
Plaintext
21274 lines
1.0 MiB
Plaintext
-- dear imgui, v1.93 WIP (Luau port)
|
|
-- (main code and documentation)
|
|
|
|
-- This file is a Lua/Luau port of Dear ImGui's imgui.cpp (docking branch, 1.93 WIP), following imgui.cpp function by function.
|
|
-- It runs on Roblox (Luau, primary target) and LÖVE (LuaJIT). Upstream documentation, examples and the demo apply almost directly.
|
|
|
|
-- Help:
|
|
-- - Call and read ImGui.ShowDemoWindow() in imgui_demo.lua. All applications in examples/ are doing that.
|
|
-- - Read top of imgui.lua for more details, links and comments.
|
|
-- - ImVec2 and ImVec4 have arithmetic metamethods (a + b, a * 2.0, etc.); fields are .x, .y (.z, .w).
|
|
-- - See README.md "Differences from C++ Dear ImGui" for the Luau conventions (out-params become return values, flags enums, text buffers).
|
|
|
|
-- Resources:
|
|
-- - FAQ ........................ https://dearimgui.com/faq (in upstream repository as docs/FAQ.md)
|
|
-- - Homepage ................... https://github.com/ocornut/imgui
|
|
-- - Releases & Changelog ....... https://github.com/ocornut/imgui/releases
|
|
-- - Gallery .................... https://github.com/ocornut/imgui/issues?q=label%3Agallery (please post your screenshots/video there!)
|
|
-- - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there)
|
|
-- - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code)
|
|
-- - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more)
|
|
-- - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings + backends for various tech/engines)
|
|
-- - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools
|
|
-- - Glossary https://github.com/ocornut/imgui/wiki/Glossary
|
|
-- - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui
|
|
-- - Issues & support ........... https://github.com/ocornut/imgui/issues (upstream; for issues specific to this port, use this port's repository)
|
|
-- - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps)
|
|
-- - Web version of the Demo .... https://pthom.github.io/imgui_explorer (w/ source code browser)
|
|
|
|
-- Copyright (c) 2014-2026 Omar Cornut
|
|
-- Developed by Omar Cornut and every direct or indirect contributors to the GitHub.
|
|
-- See LICENSE.txt for copyright and licensing details (standard MIT License).
|
|
-- This library is free but needs your support to sustain development and maintenance.
|
|
-- Businesses: you can support continued development via B2B invoiced technical support, maintenance and sponsoring contracts.
|
|
-- PLEASE reach out at omar AT discohello DOT com. See https://github.com/ocornut/imgui/wiki/Funding
|
|
-- Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine.
|
|
-- This port is unofficial and not affiliated with the Dear ImGui project; it is distributed under the same license.
|
|
|
|
-- It is recommended that you don't modify imgui.lua! It will become difficult for you to update the library.
|
|
-- Note that 'ImGui' being a plain table, you can add functions into it from your own source files, without
|
|
-- modifying imgui.lua. You may use the internal functions and data structures (imgui_internal.lua), but they don't
|
|
-- come with any guarantee of forward compatibility. Discussing your changes on the GitHub Issue Tracker may lead you
|
|
-- to a better solution or official support for them.
|
|
|
|
--
|
|
-- Index of this file:
|
|
--
|
|
-- DOCUMENTATION
|
|
--
|
|
-- - MISSION STATEMENT
|
|
-- - CONTROLS GUIDE
|
|
-- - PROGRAMMER GUIDE
|
|
-- - READ FIRST
|
|
-- - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI
|
|
-- - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE
|
|
-- - HOW A SIMPLE APPLICATION MAY LOOK LIKE
|
|
-- - USING CUSTOM BACKEND / CUSTOM ENGINE
|
|
-- - API BREAKING CHANGES (read me when you update!)
|
|
-- - FREQUENTLY ASKED QUESTIONS (FAQ)
|
|
-- - Read all answers online: https://www.dearimgui.com/faq, or in docs/FAQ.md (with a Markdown viewer)
|
|
--
|
|
-- CODE
|
|
-- (search for "[SECTION]" in the code to find them)
|
|
--
|
|
-- [SECTION] INCLUDES
|
|
-- [SECTION] FORWARD DECLARATIONS
|
|
-- [SECTION] CONTEXT AND MEMORY ALLOCATORS
|
|
-- [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO)
|
|
-- [SECTION] MISC HELPERS/UTILITIES (Geometry functions)
|
|
-- [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions)
|
|
-- [SECTION] MISC HELPERS/UTILITIES (File functions)
|
|
-- [SECTION] MISC HELPERS/UTILITIES (ImText* functions)
|
|
-- [SECTION] MISC HELPERS/UTILITIES (Color functions)
|
|
-- [SECTION] ImGuiStorage
|
|
-- [SECTION] ImGuiTextFilter
|
|
-- [SECTION] ImGuiTextBuffer, ImGuiTextIndex
|
|
-- [SECTION] ImGuiListClipper
|
|
-- [SECTION] STYLING
|
|
-- [SECTION] RENDER HELPERS
|
|
-- [SECTION] INITIALIZATION, SHUTDOWN
|
|
-- [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!)
|
|
-- [SECTION] FONTS, TEXTURES
|
|
-- [SECTION] ID STACK
|
|
-- [SECTION] INPUTS
|
|
-- [SECTION] ERROR CHECKING, STATE RECOVERY
|
|
-- [SECTION] ITEM SUBMISSION
|
|
-- [SECTION] LAYOUT
|
|
-- [SECTION] SCROLLING
|
|
-- [SECTION] TOOLTIPS
|
|
-- [SECTION] POPUPS
|
|
-- [SECTION] WINDOW FOCUS
|
|
-- [SECTION] KEYBOARD/GAMEPAD NAVIGATION
|
|
-- [SECTION] DRAG AND DROP
|
|
-- [SECTION] LOGGING/CAPTURING
|
|
-- [SECTION] SETTINGS
|
|
-- [SECTION] LOCALIZATION
|
|
-- [SECTION] VIEWPORTS, PLATFORM WINDOWS
|
|
-- [SECTION] DOCKING
|
|
-- [SECTION] PLATFORM DEPENDENT HELPERS
|
|
-- [SECTION] METRICS/DEBUGGER WINDOW
|
|
-- [SECTION] DEBUG LOG WINDOW
|
|
-- [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL)
|
|
--
|
|
-- (In this port, the sections keep upstream's names but may appear in a different order:
|
|
-- e.g. DRAG AND DROP and DOCKING are near the top of imgui.lua. Some are partial or empty, see docs/TODO.txt.)
|
|
--
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- DOCUMENTATION
|
|
-------------------------------------------------------------------------------
|
|
|
|
--
|
|
-- MISSION STATEMENT
|
|
-- =================
|
|
--
|
|
-- - Easy to use to create code-driven and data-driven tools.
|
|
-- - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools.
|
|
-- - Easy to hack and improve.
|
|
-- - Minimize setup and maintenance.
|
|
-- - Minimize state storage on user side.
|
|
-- - Minimize state synchronization.
|
|
-- - Portable, minimize dependencies, run on target (consoles, phones, etc.).
|
|
-- - Efficient runtime and memory consumption.
|
|
--
|
|
-- Designed primarily for developers and content-creators, not the typical end-user!
|
|
-- Some of the current weaknesses (which we aim to address in the future) includes:
|
|
--
|
|
-- - Doesn't look fancy by default.
|
|
-- - Limited layout features, intricate layouts are typically crafted in code.
|
|
--
|
|
--
|
|
-- CONTROLS GUIDE
|
|
-- ==============
|
|
--
|
|
-- - MOUSE CONTROLS
|
|
-- - Mouse wheel: Scroll vertically.
|
|
-- - Shift+Mouse wheel: Scroll horizontally.
|
|
-- - Click [X]: Close a window, available when an 'open' value is passed to ImGui.Begin() (open = ImGui.Begin(name, open)).
|
|
-- - Click ^, Double-Click title: Collapse window.
|
|
-- - Drag on corner/border: Resize window (double-click to auto fit window to its contents).
|
|
-- - Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true).
|
|
-- - Left-click outside popup: Close popup stack (right-click over underlying popup: Partially close popup stack).
|
|
--
|
|
-- - TEXT EDITOR
|
|
-- - Hold Shift or Drag Mouse: Select text.
|
|
-- - Ctrl+Left/Right: Word jump.
|
|
-- - Ctrl+Shift+Left/Right: Select words.
|
|
-- - Ctrl+A or Double-Click: Select All.
|
|
-- - Ctrl+X, Ctrl+C, Ctrl+V: Use OS clipboard (on Roblox, requires ImGui_ImplRoblox.SetTextProxy(true)).
|
|
-- - Ctrl+Z Undo.
|
|
-- - Ctrl+Y or Ctrl+Shift+Z: Redo.
|
|
-- - ESCAPE: Revert text to its original value.
|
|
-- - On macOS, controls are automatically adjusted to match standard macOS text editing and behaviors.
|
|
-- (for 99% of shortcuts, Ctrl is replaced by Cmd on macOS).
|
|
--
|
|
-- - KEYBOARD CONTROLS
|
|
-- - Basic:
|
|
-- - Tab, Shift+Tab Cycle through text editable fields.
|
|
-- - Ctrl+Tab, Ctrl+Shift+Tab Cycle through windows.
|
|
-- - Ctrl+Click Input text into a Slider or Drag widget.
|
|
-- - Extended features with `io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard)`:
|
|
-- - Tab, Shift+Tab: Cycle through every items.
|
|
-- - Arrow keys Move through items using directional navigation. Tweak value.
|
|
-- - Arrow keys + Alt, Shift Tweak slower, tweak faster (when using arrow keys).
|
|
-- - Enter Activate item (prefer text input when possible).
|
|
-- - Space Activate item (prefer tweaking with arrows when possible).
|
|
-- - Escape Deactivate item, leave child window, close popup.
|
|
-- - Page Up, Page Down Previous page, next page.
|
|
-- - Home, End Scroll to top, scroll to bottom.
|
|
-- - Alt Toggle between scrolling layer and menu layer.
|
|
-- - Ctrl+Tab then Ctrl+Arrows Move window. Hold Shift to resize instead of moving.
|
|
-- - Menu or Shift+F10 Open context menu.
|
|
-- - Output when ImGuiConfigFlags.NavEnableKeyboard set,
|
|
-- - io.WantCaptureKeyboard flag is set when keyboard is claimed.
|
|
-- - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags.NoNavInputs flag set.
|
|
-- - io.NavVisible: true when the navigation cursor is visible (usually goes to back false when mouse is used).
|
|
--
|
|
-- - GAMEPAD CONTROLS
|
|
-- - Enable with 'io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad)'.
|
|
-- - Particularly useful to use Dear ImGui on a console system (e.g. PlayStation, Switch, Xbox) without a mouse!
|
|
-- - Download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets
|
|
-- - Backend support: backend needs to:
|
|
-- - Set 'io.BackendFlags = bit32.bor(io.BackendFlags, ImGuiBackendFlags.HasGamepad)' + call io:AddKeyEvent()/io:AddKeyAnalogEvent() with ImGuiKey.GamepadXXX keys.
|
|
-- - For analog values (0.0 to 1.0), backend is responsible to handling a dead-zone and rescaling inputs accordingly.
|
|
-- Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.).
|
|
-- - (The Roblox and LÖVE backends of this port currently handle mouse and keyboard only: gamepad and touch are not wired up yet.)
|
|
-- - If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing,
|
|
-- with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved.
|
|
--
|
|
-- - REMOTE INPUTS SHARING & MOUSE EMULATION
|
|
-- - See https://github.com/ocornut/imgui/wiki/Useful-Extensions#remoting for other remoting solutions.
|
|
-- - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the io.ConfigNavMoveSetMousePos flag.
|
|
-- Enabling io.ConfigNavMoveSetMousePos + ImGuiBackendFlags.HasSetMousePos instructs Dear ImGui to move your mouse cursor along with navigation movements.
|
|
-- When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved.
|
|
-- When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame.
|
|
-- (If you set the io.ConfigNavMoveSetMousePos flag but don't honor 'io.WantSetMousePos' properly, Dear ImGui will misbehave as it will see your mouse moving back & forth!)
|
|
--
|
|
--
|
|
-- PROGRAMMER GUIDE
|
|
-- ================
|
|
--
|
|
-- READ FIRST
|
|
-- ----------
|
|
-- - Remember to check the wonderful Wiki: https://github.com/ocornut/imgui/wiki
|
|
-- - Your code creates the UI every frame of your application loop, if your code doesn't run the UI is gone!
|
|
-- The UI can be highly dynamic, there are no construction or destruction steps, less superfluous
|
|
-- data retention on your side, less state duplication, less state synchronization, fewer bugs.
|
|
-- - Call and read ImGui.ShowDemoWindow() for demo code demonstrating most features (imgui_demo.lua, a port of imgui_demo.cpp).
|
|
-- Or browse pthom's online imgui_explorer: https://pthom.github.io/imgui_explorer for a web version w/ source code browser.
|
|
-- - Dear ImGui is an implementation of the IMGUI paradigm (immediate-mode graphical user interface, a term coined by Casey Muratori).
|
|
-- You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links in Wiki.
|
|
-- - Dear ImGui is a "single pass" rasterizing implementation of the IMGUI paradigm, aimed at ease of use and high-performances.
|
|
-- For every application frame, your UI code will be called only once. This is in contrast to e.g. Unity's implementation of an IMGUI,
|
|
-- where the UI code is called multiple times ("multiple passes") from a single entry point. There are pros and cons to both approaches.
|
|
-- - Our origin is on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right.
|
|
-- - IM_ASSERT() is used throughout the code, like upstream.
|
|
-- If you get an assert, read the messages and comments around the assert.
|
|
-- - This codebase aims to be highly optimized:
|
|
-- - A typical idle frame should allocate as little as possible (in Luau: avoid creating tables/closures per frame).
|
|
-- - We rely on a maximum of constant-time or O(N) algorithms. Limiting searches/scans as much as possible.
|
|
-- - This codebase aims to be both highly opinionated and highly flexible:
|
|
-- - This code works because of the things it choose to solve or not solve.
|
|
-- - The port keeps upstream's structure (ImGui is a table of free functions, structures are plain tables)
|
|
-- so that upstream code, comments and documentation map almost 1:1. See docs/PORTING.md for the porting rules.
|
|
--
|
|
--
|
|
-- HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI
|
|
-- ----------------------------------------------
|
|
-- - Update by re-pulling this repository (or updating the Wally package / downloading a newer release),
|
|
-- then rebuild the bundle (python3 tools/bundle.py, or --target love) and overwrite every file.
|
|
-- - This port tracks upstream; read upstream's "API BREAKING CHANGES" (see below) when the tracked upstream version changes.
|
|
-- If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed
|
|
-- from the public API. If you have a problem with a missing function/symbols, search for its name in the code, there will
|
|
-- likely be a comment about it. Please report any issue to the GitHub page!
|
|
-- - Try to keep your copy of Dear ImGui reasonably up to date!
|
|
--
|
|
--
|
|
-- GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE
|
|
-- ---------------------------------------------------------------
|
|
-- - See https://github.com/ocornut/imgui/wiki/Getting-Started, and README.md of this repository.
|
|
-- - Run and study the examples and demo in imgui_demo.lua to get acquainted with the library.
|
|
-- - In the majority of cases you should be able to use unmodified backends files available in the backends/ folder
|
|
-- (backends/imgui_impl_roblox.lua for Roblox, backends/imgui_impl_love.lua for LÖVE).
|
|
-- - Build a single file with tools/bundle.py: build/ImGui.luau (Roblox ModuleScript) or build/imgui_love.lua (LÖVE),
|
|
-- then require() / load it. All exported names (ImGui, ImVec2, ImVec4, IM_COL32, every ImGui*Flags enum...) are fields of the returned table.
|
|
-- - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them.
|
|
-- - Dear ImGui never touches or knows about your GPU state. The only function that knows about rendering is the draw function that you provide (the backend's RenderDrawData).
|
|
-- Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render"
|
|
-- phases of your own application. All rendering information is stored into command-lists that you will retrieve after calling ImGui.Render().
|
|
-- - Refer to the backends and demo applications in the examples/ folder for instruction on how to setup your code.
|
|
--
|
|
--
|
|
-- HOW A SIMPLE APPLICATION MAY LOOK LIKE
|
|
-- --------------------------------------
|
|
--
|
|
-- USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.lua files from the backends/ folder).
|
|
-- The sub-folders in examples/ contain examples applications following this structure.
|
|
--
|
|
-- Roblox (LocalScript, see examples/example_roblox/main.client.luau):
|
|
--
|
|
-- local G = require(ReplicatedStorage:WaitForChild("ImGui"))
|
|
-- local ImGui, ImGui_ImplRoblox = G.ImGui, G.ImGui_ImplRoblox
|
|
--
|
|
-- -- Application init: create a dear imgui context, setup some options, load fonts
|
|
-- ImGui.CreateContext()
|
|
-- local io = ImGui.GetIO()
|
|
-- -- TODO: Set optional io.ConfigFlags values, e.g. 'io.ConfigFlags = bit32.bor(io.ConfigFlags, G.ImGuiConfigFlags.NavEnableKeyboard)' to enable keyboard controls.
|
|
-- -- TODO: Fill optional fields of the io structure later.
|
|
-- -- TODO: Load TTF/OTF fonts if you don't want to use the default font.
|
|
--
|
|
-- -- Initialize helper Platform and Renderer backend (here we are using imgui_impl_roblox.lua)
|
|
-- ImGui_ImplRoblox.Init()
|
|
--
|
|
-- -- Application main loop
|
|
-- RunService.Stepped:Connect(function()
|
|
-- -- Feed inputs to dear imgui, start new frame
|
|
-- ImGui_ImplRoblox.NewFrame()
|
|
-- ImGui.NewFrame()
|
|
--
|
|
-- -- Any application code here
|
|
-- ImGui.Text("Hello, world!")
|
|
--
|
|
-- -- Render dear imgui
|
|
-- ImGui.Render()
|
|
-- ImGui_ImplRoblox.RenderDrawData(ImGui.GetDrawData())
|
|
-- end)
|
|
--
|
|
-- -- Shutdown
|
|
-- ImGui_ImplRoblox.Shutdown()
|
|
-- ImGui.DestroyContext()
|
|
--
|
|
-- LÖVE (see examples/example_love/main.lua):
|
|
--
|
|
-- function love.load()
|
|
-- ImGui.CreateContext()
|
|
-- G.ImGui_ImplLove.Init({ root = ROOT })
|
|
-- end
|
|
-- function love.update(dt)
|
|
-- G.ImGui_ImplLove.NewFrame()
|
|
-- ImGui.NewFrame()
|
|
-- ImGui.Text("Hello, world!")
|
|
-- ImGui.Render()
|
|
-- end
|
|
-- function love.draw()
|
|
-- G.ImGui_ImplLove.RenderDrawData(ImGui.GetDrawData())
|
|
-- end
|
|
-- -- + forward love.mousemoved/mousepressed/mousereleased/wheelmoved/keypressed/keyreleased/textinput/focus
|
|
-- -- to G.ImGui_ImplLove.MouseMoved/MousePressed/MouseReleased/WheelMoved/KeyPressed/KeyReleased/TextInput/Focus.
|
|
--
|
|
-- To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application,
|
|
-- you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags!
|
|
-- Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this.
|
|
--
|
|
--
|
|
-- USAGE
|
|
-- -----
|
|
--
|
|
-- Widget functions are called every frame; their return values tell you what the user did.
|
|
-- Out-parameters of the C++ API (bool* p_open, float* v, int* v...) are passed by value and the new value is returned:
|
|
--
|
|
-- ImGui.Text("Hello, world %d", 123)
|
|
-- if ImGui.Button("Save") then
|
|
-- MySaveFunction()
|
|
-- end
|
|
-- ImGui.InputText("string", buf, 256) -- buf = ImGui.NewTextBuffer("...", 256), edited in place
|
|
-- f = ImGui.SliderFloat("float", f, 0.0, 1.0) -- returns (value, changed)
|
|
-- pressed, v = ImGui.Checkbox("checkbox", v)
|
|
--
|
|
-- -- Create a window called "My First Tool", with a menu bar.
|
|
-- my_tool_active = ImGui.Begin("My First Tool", my_tool_active, ImGuiWindowFlags.MenuBar) -- returns (open, visible)
|
|
-- if ImGui.BeginMenuBar() then
|
|
-- if ImGui.BeginMenu("File") then
|
|
-- if ImGui.MenuItem("Close", "Ctrl+W") then my_tool_active = false end
|
|
-- ImGui.EndMenu()
|
|
-- end
|
|
-- ImGui.EndMenuBar()
|
|
-- end
|
|
-- ImGui.ColorEdit4("Color", my_color) -- ImVec4 / arrays are edited in place
|
|
-- ImGui.End()
|
|
--
|
|
--
|
|
-- USING CUSTOM BACKEND / CUSTOM ENGINE
|
|
-- ------------------------------------
|
|
--
|
|
-- A backend implements the usual Dear ImGui contract: Init, NewFrame, RenderDrawData, inputs -> io:AddXXXEvent(),
|
|
-- texture updates via ImTextureData. Adding another Lua host means a new backends/imgui_impl_<name>.lua.
|
|
-- -> see https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md for basic instructions.
|
|
-- -> the backends in backends/imgui_impl_roblox.lua and backends/imgui_impl_love.lua are complete implementations.
|
|
--
|
|
-- Basic application/backend skeleton:
|
|
--
|
|
-- -- Application init: create a Dear ImGui context, setup some options, load fonts
|
|
-- ImGui.CreateContext()
|
|
-- local io = ImGui.GetIO()
|
|
-- -- TODO: set io.ConfigXXX values, e.g.
|
|
-- io.ConfigFlags = bit32.bor(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) -- Enable keyboard controls
|
|
--
|
|
-- -- TODO: Load TTF/OTF fonts if you don't want to use the default font.
|
|
-- io.Fonts:AddFontFromFileTTF("fonts/Roboto-Medium.ttf", 16.0)
|
|
--
|
|
-- -- Application main loop (called once per frame)
|
|
-- local function OnFrame(dt)
|
|
-- -- Setup low-level inputs, or write to those fields from your input event handlers, etc.
|
|
-- -- (In the examples/ app this is usually done within the ImGui_ImplXXX.NewFrame() function from one of the Backends)
|
|
-- io.DeltaTime = dt -- set the time elapsed since the previous frame (in seconds)
|
|
-- io.DisplaySize.x = 1920.0 -- set the current display width
|
|
-- io.DisplaySize.y = 1280.0 -- set the current display height here
|
|
-- io:AddMousePosEvent(mouse_x, mouse_y) -- update mouse position
|
|
-- io:AddMouseButtonEvent(0, mouse_b0) -- update mouse button states
|
|
-- io:AddMouseButtonEvent(1, mouse_b1) -- update mouse button states
|
|
--
|
|
-- -- Call NewFrame(), after this point you can use ImGui.* functions anytime
|
|
-- -- (So you want to try calling NewFrame() as early as you can in your main loop to be able to use Dear ImGui everywhere)
|
|
-- ImGui.NewFrame()
|
|
--
|
|
-- -- Most of your application code here
|
|
-- ImGui.Text("Hello, world!")
|
|
-- MyGameUpdate() -- may use any Dear ImGui functions, e.g. ImGui.Begin("My window"); ImGui.Text("Hello, world!"); ImGui.End()
|
|
-- MyGameRender() -- may use any Dear ImGui functions as well!
|
|
--
|
|
-- -- End the dear imgui frame
|
|
-- -- (You want to try calling EndFrame/Render as late as you can, to be able to use Dear ImGui in your own game rendering code)
|
|
-- ImGui.EndFrame() -- this is automatically called by Render(), but available
|
|
-- ImGui.Render()
|
|
--
|
|
-- -- Update textures
|
|
-- local draw_data = ImGui.GetDrawData()
|
|
-- for _, tex in draw_data.Textures:iter() do
|
|
-- if tex.Status ~= ImTextureStatus.OK then
|
|
-- MyImGuiBackend_UpdateTexture(tex)
|
|
-- end
|
|
-- end
|
|
--
|
|
-- -- Render dear imgui contents
|
|
-- MyImGuiBackend_RenderDrawData(draw_data)
|
|
-- end
|
|
--
|
|
-- -- Shutdown
|
|
-- ImGui.DestroyContext()
|
|
--
|
|
--
|
|
--
|
|
-- API BREAKING CHANGES
|
|
-- ====================
|
|
--
|
|
-- This port tracks upstream Dear ImGui 1.93 WIP (docking branch).
|
|
-- For the change-log of API breaking changes, see the top of upstream imgui.cpp, or https://github.com/ocornut/imgui/releases
|
|
--
|
|
--
|
|
-- FREQUENTLY ASKED QUESTIONS (FAQ)
|
|
-- ================================
|
|
--
|
|
-- Read all answers online:
|
|
-- https://www.dearimgui.com/faq or https://github.com/ocornut/imgui/blob/master/docs/FAQ.md (same url)
|
|
-- For questions specific to this port (Roblox, LÖVE, performance, differences from C++), see the FAQ in README.md of this repository.
|
|
--
|
|
|
|
--- Dear ImGui WIP
|
|
-- (Core Code)
|
|
|
|
--- Flags:
|
|
-- IMGUI_DISABLE_OBSOLETE_FUNCTIONS = true -- This flag is not implemented
|
|
|
|
-- NOTE: you must implement ImStd.ImFileOpen if you are not in GMod
|
|
IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS = true -- Don't implement ImStd.ImFileOpen/ImStd.ImFileClose/ImStd.ImFileRead/ImFileWrite so you can implement them yourself
|
|
|
|
IMGUI_DISABLE_DEBUG_TOOLS = false -- Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty
|
|
|
|
-- Current context pointer. Implicitly used by all Dear ImGui functions. Always assumed to be ~= nil.
|
|
-- - ImGui.CreateContext() will automatically set this pointer if it is nil.
|
|
-- Change to a different context by calling ImGui.SetCurrentContext().
|
|
--- @type ImGuiContext?
|
|
local GImGui = nil
|
|
|
|
ImGui = {}
|
|
|
|
ImStd = {} -- Contains functions that originally don't belong to cpp namespaces
|
|
|
|
-- IM_INCLUDE(name) is provided by the bundle (see tools/bundle.py): runs the bundled file once, returns its result.
|
|
|
|
function ImGui._GetTypeFunc() return type end
|
|
|
|
local function LUA_TableConstructorWrapper(T, size, userdata)
|
|
local p = {}
|
|
for i = 1, size do p[i] = T() end
|
|
return p
|
|
end
|
|
|
|
-- Memory Allocator functions. Use SetAllocatorFunctions() to change them.
|
|
-- - You probably don't want to modify that mid-program.
|
|
local GImAllocatorAllocFunc = LUA_TableConstructorWrapper
|
|
local GImAllocatorFreeFunc = function(p0, p1, p2) end
|
|
local GImAllocatorUserData = nil
|
|
|
|
-- IM_ALLOC() == ImGui.MemAlloc()
|
|
--- @param constructor function
|
|
--- @param size int
|
|
function ImGui.MemAlloc(constructor, size)
|
|
local p = GImAllocatorAllocFunc(constructor, size, GImAllocatorUserData)
|
|
local ctx = GImGui
|
|
if ctx then
|
|
ImGui.DebugAllocHook(ctx.DebugAllocInfo, ctx.FrameCount, p, size)
|
|
end
|
|
return p
|
|
end
|
|
|
|
-- IM_FREE() == ImGui.MemFree()
|
|
-- This does not set `owner[field]` to nil!
|
|
--- @param owner table
|
|
--- @param field string|int
|
|
function ImGui.MemFree(owner, field)
|
|
if owner ~= nil then
|
|
local ctx = GImGui
|
|
if ctx then
|
|
ImGui.DebugAllocHook(ctx.DebugAllocInfo, ctx.FrameCount, owner, -1)
|
|
end
|
|
end
|
|
return GImAllocatorFreeFunc(owner, field, GImAllocatorUserData)
|
|
end
|
|
|
|
-- We record the number of allocation in recent frames, as a way to audit/sanitize our guiding principles of "no allocations on idle/repeating frames"
|
|
--- @param info ImGuiDebugAllocInfo
|
|
--- @param frame_count int
|
|
--- @param ptr table
|
|
--- @param size size_t
|
|
function ImGui.DebugAllocHook(info, frame_count, ptr, size)
|
|
local entry = info.LastEntriesBuf[info.LastEntriesIdx]
|
|
if entry.FrameCount ~= frame_count then
|
|
info.LastEntriesIdx = info.LastEntriesIdx % (#info.LastEntriesBuf) + 1
|
|
entry = info.LastEntriesBuf[info.LastEntriesIdx]
|
|
entry.FrameCount = frame_count
|
|
entry.AllocCount = 0
|
|
entry.FreeCount = 0
|
|
end
|
|
if size ~= -1 then
|
|
-- print(string.format("[%05d] MemAlloc(%d) -> %p", frame_count, size, ptr))
|
|
entry.AllocCount = entry.AllocCount + 1
|
|
info.TotalAllocCount = info.TotalAllocCount + 1
|
|
else
|
|
-- print(string.format("[%05d] MemFree(%p)", frame_count, ptr))
|
|
entry.FreeCount = entry.FreeCount + 1
|
|
info.TotalFreeCount = info.TotalFreeCount + 1
|
|
end
|
|
end
|
|
|
|
IM_INCLUDE"imgui_h.lua"
|
|
|
|
IM_INCLUDE"imgui_internal.lua"
|
|
|
|
IM_INCLUDE"imgui_draw.lua"
|
|
|
|
IM_INCLUDE"imgui_widgets.lua"
|
|
|
|
IM_INCLUDE"imgui_tables.lua"
|
|
|
|
|
|
do --[[ [SECTION] imgui_dragdrop (was imgui_dragdrop.lua) ]]
|
|
--- Dear ImGui
|
|
-- Drag and drop: 1:1 port of imgui.cpp [SECTION] DRAG AND DROP (docking branch)
|
|
-- Payload `Data` is any Lua value (no copy is made). `data_size` is optional and only kept for API parity.
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] DRAG AND DROP
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.IsDragDropActive()
|
|
return ImGui.GetCurrentContext().DragDropActive
|
|
end
|
|
|
|
function ImGui.ClearDragDrop()
|
|
local g = ImGui.GetCurrentContext()
|
|
g.DragDropActive = false
|
|
ImGuiPayload_Clear(g.DragDropPayload)
|
|
g.DragDropAcceptFlagsCurr = ImGuiDragDropFlags.None
|
|
g.DragDropAcceptIdCurr = 0
|
|
g.DragDropAcceptIdPrev = 0
|
|
g.DragDropAcceptIdCurrRectSurface = FLT_MAX
|
|
g.DragDropAcceptFrameCount = -1
|
|
end
|
|
|
|
-- When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource()
|
|
-- If the item has an identifier:
|
|
-- - This assume/require the item to be activated (typically via ButtonBehavior).
|
|
-- - Therefore if you want to use this with a mouse button other than left mouse button, it is up to the item itself to activate with another button.
|
|
-- - We then pull and use the mouse button that was used to activate the item and use it to carry on the drag.
|
|
-- If the item has no identifier:
|
|
-- - Currently always assume left mouse button.
|
|
function ImGui.BeginDragDropSource(flags)
|
|
if flags == nil then flags = 0 end
|
|
local g = ImGui.GetCurrentContext()
|
|
local window = g.CurrentWindow
|
|
|
|
-- FIXME-DRAGDROP: While in the common-most "drag from non-zero active id" case we can tell the mouse button,
|
|
-- in both SourceExtern and id==0 cases we may requires something else (explicit flags or some heuristic).
|
|
local mouse_button = ImGuiMouseButton.Left
|
|
|
|
local source_drag_active = false
|
|
local source_id = 0
|
|
local source_parent_id = 0
|
|
if bit32.band(flags, ImGuiDragDropFlags.SourceExtern) == 0 then
|
|
source_id = g.LastItemData.ID
|
|
if source_id ~= 0 then
|
|
-- Common path: items with ID
|
|
if g.ActiveId ~= source_id then return false end
|
|
if g.ActiveIdMouseButton ~= -1 then mouse_button = g.ActiveIdMouseButton end
|
|
if g.IO.MouseDown[mouse_button] == false or window.SkipItems then return false end
|
|
g.ActiveIdAllowOverlap = false
|
|
else
|
|
-- Uncommon path: items without ID
|
|
if g.IO.MouseDown[mouse_button] == false or window.SkipItems then return false end
|
|
if bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredRect) == 0 and (g.ActiveId == 0 or g.ActiveIdWindow ~= window) then
|
|
return false
|
|
end
|
|
|
|
-- If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to:
|
|
-- A) Read the explanation below, B) Use the ImGuiDragDropFlags.SourceAllowNullID flag.
|
|
if bit32.band(flags, ImGuiDragDropFlags.SourceAllowNullID) == 0 then
|
|
IM_ASSERT(false, "Use ImGuiDragDropFlags.SourceAllowNullID for items without an ID")
|
|
return false
|
|
end
|
|
|
|
-- Magic fallback to handle items with no assigned ID, e.g. Text(), Image()
|
|
-- We build a throwaway ID based on current ID stack + relative AABB of items in window.
|
|
-- THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING/RESIZING OF THE WIDGET, so if your widget moves your dragging operation will be canceled.
|
|
-- We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive.
|
|
-- Rely on keeping other window.LastItemXXX fields intact.
|
|
source_id = window:GetIDFromRectangle(g.LastItemData.Rect)
|
|
g.LastItemData.ID = source_id
|
|
ImGui.KeepAliveID(source_id)
|
|
local is_hovered = ImGui.ItemHoverable(g.LastItemData.Rect, source_id, g.LastItemData.ItemFlags)
|
|
if is_hovered and g.IO.MouseClicked[mouse_button] then
|
|
ImGui.SetActiveID(source_id, window)
|
|
ImGui.FocusWindow(window)
|
|
end
|
|
if g.ActiveId == source_id then g.ActiveIdAllowOverlap = is_hovered end -- Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker.
|
|
end
|
|
if g.ActiveId ~= source_id then return false end
|
|
source_parent_id = window.IDStack.Data[window.IDStack.Size]
|
|
source_drag_active = ImGui.IsMouseDragging(mouse_button)
|
|
|
|
-- Disable navigation and key inputs while dragging + cancel existing request if any
|
|
ImGui.SetActiveIdUsingAllKeyboardKeys()
|
|
else
|
|
-- When ImGuiDragDropFlags.SourceExtern is set:
|
|
window = nil
|
|
source_id = ImHashStr("#SourceExtern")
|
|
source_drag_active = true
|
|
mouse_button = g.IO.MouseDown[0] and 0 or -1
|
|
ImGui.KeepAliveID(source_id)
|
|
ImGui.SetActiveID(source_id, nil)
|
|
end
|
|
|
|
IM_ASSERT(g.DragDropWithinTarget == false) -- Can't nest BeginDragDropSource() and BeginDragDropTarget()
|
|
if not source_drag_active then return false end
|
|
|
|
-- Activate drag and drop
|
|
if not g.DragDropActive then
|
|
IM_ASSERT(source_id ~= 0)
|
|
ImGui.ClearDragDrop()
|
|
local payload = g.DragDropPayload
|
|
payload.SourceId = source_id
|
|
payload.SourceParentId = source_parent_id
|
|
g.DragDropActive = true
|
|
g.DragDropSourceFlags = flags
|
|
g.DragDropMouseButton = mouse_button
|
|
if payload.SourceId == g.ActiveId then g.ActiveIdNoClearOnFocusLoss = true end
|
|
end
|
|
g.DragDropSourceFrameCount = g.FrameCount
|
|
g.DragDropWithinSource = true
|
|
|
|
if bit32.band(flags, ImGuiDragDropFlags.SourceNoPreviewTooltip) == 0 then
|
|
-- Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit)
|
|
-- We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents.
|
|
local ret
|
|
if g.DragDropAcceptIdPrev ~= 0 and bit32.band(g.DragDropAcceptFlagsPrev, ImGuiDragDropFlags.AcceptNoPreviewTooltip) ~= 0 then
|
|
ret = ImGui.BeginTooltipHidden()
|
|
else
|
|
ret = ImGui.BeginTooltip()
|
|
end
|
|
IM_ASSERT(ret) -- FIXME-NEWBEGIN: If this ever becomes false, we need to Begin("##Hidden", nil, ImGuiWindowFlags.NoSavedSettings) + SetWindowHiddenAndSkipItemsForCurrentFrame().
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiDragDropFlags.SourceNoDisableHover) == 0 and bit32.band(flags, ImGuiDragDropFlags.SourceExtern) == 0 then
|
|
g.LastItemData.StatusFlags = bit32.band(g.LastItemData.StatusFlags, bit32.bnot(ImGuiItemStatusFlags.HoveredRect))
|
|
end
|
|
return true
|
|
end
|
|
|
|
function ImGui.EndDragDropSource()
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(g.DragDropActive)
|
|
IM_ASSERT(g.DragDropWithinSource, "Not after a BeginDragDropSource()?")
|
|
|
|
if bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.SourceNoPreviewTooltip) == 0 then
|
|
ImGui.EndTooltip()
|
|
end
|
|
|
|
-- Discard the drag if have not called SetDragDropPayload()
|
|
if g.DragDropPayload.DataFrameCount == -1 then ImGui.ClearDragDrop() end
|
|
g.DragDropWithinSource = false
|
|
end
|
|
|
|
-- Use 'cond' to choose to submit payload on drag start or every frame
|
|
--- @param data any # any Lua value
|
|
--- @return bool accepted
|
|
function ImGui.SetDragDropPayload(type, data, data_size, cond)
|
|
local g = ImGui.GetCurrentContext()
|
|
local payload = g.DragDropPayload
|
|
if cond == nil or cond == 0 then cond = ImGuiCond.Always end
|
|
|
|
IM_ASSERT(type ~= nil)
|
|
IM_ASSERT(#type < 32, "Payload type can be at most 32 characters long")
|
|
IM_ASSERT(cond == ImGuiCond.Always or cond == ImGuiCond.Once)
|
|
IM_ASSERT(payload.SourceId ~= 0) -- Not called between BeginDragDropSource() and EndDragDropSource()
|
|
|
|
if cond == ImGuiCond.Always or payload.DataFrameCount == -1 then
|
|
-- Copy payload
|
|
payload.DataType = type
|
|
payload.Data = data
|
|
payload.DataSize = data_size or ((data ~= nil) and 1 or 0)
|
|
end
|
|
payload.DataFrameCount = g.FrameCount
|
|
|
|
-- Return whether the payload has been accepted
|
|
return (g.DragDropAcceptFrameCount == g.FrameCount) or (g.DragDropAcceptFrameCount == g.FrameCount - 1)
|
|
end
|
|
|
|
function ImGui.BeginDragDropTargetCustom(bb, id)
|
|
local g = ImGui.GetCurrentContext()
|
|
if not g.DragDropActive then return false end
|
|
|
|
local window = g.CurrentWindow
|
|
local hovered_window = g.HoveredWindowUnderMovingWindow
|
|
if hovered_window == nil or window.RootWindowDockTree ~= hovered_window.RootWindowDockTree then return false end
|
|
IM_ASSERT(id ~= 0)
|
|
if not ImGui.IsMouseHoveringRect(bb.Min, bb.Max) or (id == g.DragDropPayload.SourceId) then return false end
|
|
if window.SkipItems then return false end
|
|
|
|
IM_ASSERT(g.DragDropWithinTarget == false and g.DragDropWithinSource == false) -- Can't nest BeginDragDropSource() and BeginDragDropTarget()
|
|
g.DragDropTargetRect = ImRect(bb.Min, bb.Max)
|
|
g.DragDropTargetClipRect = ImRect(window.ClipRect.Min, window.ClipRect.Max) -- May want to be overridden by user depending on use case?
|
|
g.DragDropTargetId = id
|
|
g.DragDropTargetFullViewport = 0
|
|
g.DragDropWithinTarget = true
|
|
return true
|
|
end
|
|
|
|
-- Typical usage would be:
|
|
-- if not ImGui.IsWindowHovered(bit32.bor(ImGuiHoveredFlags.AnyWindow, ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) then
|
|
-- if ImGui.BeginDragDropTargetViewport(ImGui.GetMainViewport(), nil) then
|
|
-- But we are leaving the hover test to the caller for maximum flexibility.
|
|
function ImGui.BeginDragDropTargetViewport(viewport, p_bb)
|
|
local g = ImGui.GetCurrentContext()
|
|
if not g.DragDropActive then return false end
|
|
|
|
local bb = p_bb or viewport:GetWorkRect()
|
|
local id = viewport.ID
|
|
if g.MouseViewport ~= viewport or not ImGui.IsMouseHoveringRect(bb.Min, bb.Max, false) or (id == g.DragDropPayload.SourceId) then
|
|
return false
|
|
end
|
|
|
|
IM_ASSERT(g.DragDropWithinTarget == false and g.DragDropWithinSource == false) -- Can't nest BeginDragDropSource() and BeginDragDropTarget()
|
|
g.DragDropTargetRect = ImRect(bb.Min, bb.Max)
|
|
g.DragDropTargetClipRect = ImRect(bb.Min, bb.Max)
|
|
g.DragDropTargetId = id
|
|
g.DragDropTargetFullViewport = id
|
|
g.DragDropWithinTarget = true
|
|
return true
|
|
end
|
|
|
|
-- We don't use BeginDragDropTargetCustom() and duplicate its code because:
|
|
-- 1) we use LastItemData's ImGuiItemStatusFlags.HoveredRect which handles items that push a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them.
|
|
-- 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can.
|
|
-- Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case)
|
|
function ImGui.BeginDragDropTarget()
|
|
local g = ImGui.GetCurrentContext()
|
|
if not g.DragDropActive then return false end
|
|
|
|
local window = g.CurrentWindow
|
|
if bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredRect) == 0 then return false end
|
|
local hovered_window = g.HoveredWindowUnderMovingWindow
|
|
if hovered_window == nil or window.RootWindowDockTree ~= hovered_window.RootWindowDockTree or window.SkipItems then return false end
|
|
|
|
local display_rect = (bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HasDisplayRect) ~= 0) and g.LastItemData.DisplayRect or g.LastItemData.Rect
|
|
local id = g.LastItemData.ID
|
|
if id == 0 then
|
|
id = window:GetIDFromRectangle(display_rect)
|
|
ImGui.KeepAliveID(id)
|
|
end
|
|
if g.DragDropPayload.SourceId == id then return false end
|
|
|
|
IM_ASSERT(g.DragDropWithinTarget == false and g.DragDropWithinSource == false) -- Can't nest BeginDragDropSource() and BeginDragDropTarget()
|
|
g.DragDropTargetRect = ImRect(display_rect.Min, display_rect.Max)
|
|
local clip = (bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HasClipRect) ~= 0) and g.LastItemData.ClipRect or window.ClipRect
|
|
g.DragDropTargetClipRect = ImRect(clip.Min, clip.Max)
|
|
g.DragDropTargetId = id
|
|
g.DragDropWithinTarget = true
|
|
return true
|
|
end
|
|
|
|
function ImGui.IsDragDropPayloadBeingAccepted()
|
|
local g = ImGui.GetCurrentContext()
|
|
return g.DragDropActive and g.DragDropAcceptIdPrev ~= 0
|
|
end
|
|
|
|
--- @return ImGuiPayload?
|
|
function ImGui.AcceptDragDropPayload(type, flags)
|
|
if flags == nil then flags = 0 end
|
|
local g = ImGui.GetCurrentContext()
|
|
local payload = g.DragDropPayload
|
|
IM_ASSERT(g.DragDropActive) -- Not called between BeginDragDropTarget() and EndDragDropTarget() ?
|
|
IM_ASSERT(payload.DataFrameCount ~= -1) -- Forgot to call EndDragDropTarget() ?
|
|
if type ~= nil and not ImGuiPayload_IsDataType(payload, type) then return nil end
|
|
|
|
-- Accept smallest drag target bounding box, this allows us to nest drag targets conveniently without ordering constraints.
|
|
-- NB: We currently accept nil id as target. However, overlapping targets requires a unique ID to function!
|
|
local was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId)
|
|
local r = g.DragDropTargetRect
|
|
local r_surface = r:GetWidth() * r:GetHeight()
|
|
if r_surface > g.DragDropAcceptIdCurrRectSurface then return nil end
|
|
|
|
g.DragDropAcceptFlagsCurr = flags
|
|
g.DragDropAcceptIdCurr = g.DragDropTargetId
|
|
g.DragDropAcceptIdCurrRectSurface = r_surface
|
|
-- IMGUI_DEBUG_LOG("AcceptDragDropPayload(): %08X: accept\n", g.DragDropTargetId)
|
|
|
|
-- Render default drop visuals
|
|
payload.Preview = was_accepted_previously
|
|
flags = bit32.bor(flags, bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.AcceptNoDrawDefaultRect)) -- Source can also inhibit the preview (useful for external sources that live for 1 frame)
|
|
local draw_target_rect = payload.Preview and bit32.band(flags, ImGuiDragDropFlags.AcceptNoDrawDefaultRect) == 0
|
|
if draw_target_rect and g.DragDropTargetFullViewport ~= 0 then
|
|
ImGui.RenderDragDropTargetRectForViewport(g.DragDropTargetFullViewport, g.DragDropTargetRect)
|
|
elseif draw_target_rect then
|
|
ImGui.RenderDragDropTargetRectForItem(r)
|
|
end
|
|
|
|
g.DragDropAcceptFrameCount = g.FrameCount
|
|
if bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.SourceExtern) ~= 0 and g.DragDropMouseButton == -1 then
|
|
payload.Delivery = was_accepted_previously and (g.DragDropSourceFrameCount < g.FrameCount)
|
|
else
|
|
payload.Delivery = was_accepted_previously and not ImGui.IsMouseDown(g.DragDropMouseButton) -- For extern drag sources affecting OS window focus, it's easier to just test not IsMouseDown() instead of IsMouseReleased()
|
|
end
|
|
if not payload.Delivery and bit32.band(flags, ImGuiDragDropFlags.AcceptBeforeDelivery) == 0 then return nil end
|
|
return payload
|
|
end
|
|
|
|
-- FIXME-STYLE FIXME-DRAGDROP: Settle on a proper default visuals for drop target.
|
|
function ImGui.RenderDragDropTargetRectForItem(bb)
|
|
local g = ImGui.GetCurrentContext()
|
|
local window = g.CurrentWindow
|
|
local bb_display = ImRect(bb.Min, bb.Max)
|
|
bb_display:ClipWith(g.DragDropTargetClipRect) -- Clip THEN expand so we have a way to visualize that target is not entirely visible.
|
|
bb_display:Expand(g.Style.DragDropTargetPadding)
|
|
local push_clip_rect = not window.ClipRect:Contains(bb_display)
|
|
if push_clip_rect then window.DrawList:PushClipRectFullScreen() end
|
|
ImGui.RenderDragDropTargetRectEx(window.DrawList, bb_display, g.Style.DragDropTargetRounding)
|
|
if push_clip_rect then window.DrawList:PopClipRect() end
|
|
end
|
|
|
|
function ImGui.RenderDragDropTargetRectForViewport(viewport_id, bb)
|
|
local g = ImGui.GetCurrentContext()
|
|
local viewport = ImGui.FindViewportByID(viewport_id)
|
|
IM_ASSERT(viewport ~= nil)
|
|
local bb_padded = ImRect(bb.Min, bb.Max)
|
|
bb_padded:Expand(-g.Style.DragDropTargetPadding)
|
|
ImGui.RenderDragDropTargetRectEx(ImGui.GetForegroundDrawList(viewport), bb_padded, g.Style.DragDropTargetRounding)
|
|
end
|
|
|
|
function ImGui.RenderDragDropTargetRectEx(draw_list, bb, rounding)
|
|
local g = ImGui.GetCurrentContext()
|
|
draw_list:AddRectFilled(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTargetBg), rounding, 0)
|
|
draw_list:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.DragDropTarget), rounding, g.Style.DragDropTargetBorderSize)
|
|
end
|
|
|
|
--- @return ImGuiPayload?
|
|
function ImGui.GetDragDropPayload()
|
|
local g = ImGui.GetCurrentContext()
|
|
return (g.DragDropActive and g.DragDropPayload.DataFrameCount ~= -1) and g.DragDropPayload or nil
|
|
end
|
|
|
|
function ImGui.EndDragDropTarget()
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(g.DragDropActive)
|
|
IM_ASSERT(g.DragDropWithinTarget)
|
|
g.DragDropWithinTarget = false
|
|
|
|
-- Clear drag and drop state payload right after delivery
|
|
if g.DragDropPayload.Delivery then ImGui.ClearDragDrop() end
|
|
end
|
|
|
|
-- Payload methods (C++ ImGuiPayload members): payload:IsDataType(t) etc. also work through these globals
|
|
function ImGui.PayloadIsDataType(payload, t) return ImGuiPayload_IsDataType(payload, t) end
|
|
end --[[ [SECTION] imgui_dragdrop (was imgui_dragdrop.lua) ]]
|
|
|
|
do --[[ [SECTION] imgui_docking (was imgui_docking.lua) ]]
|
|
--- Dear ImGui
|
|
-- Docking: 1:1 port of imgui.cpp [SECTION] DOCKING (docking branch, 1.93 WIP)
|
|
--
|
|
-- Port notes:
|
|
-- - Main viewport only (no multi-viewports on Roblox): viewport creation/merging code paths are kept where cheap, but
|
|
-- windows always live in the main viewport.
|
|
-- - ImGuiDockContext.Nodes (ImGuiStorage in C++) is a Lua table ID -> node. Iterations that upstream does over the
|
|
-- sorted storage are done over a sorted snapshot of the IDs (DockContextNodesSorted) for deterministic order.
|
|
-- - node.ChildNodes is a table indexed [0] and [1] like upstream.
|
|
-- - ImGuiDockPreviewData.DropRectsDraw is indexed [dir + 1] with dir in -1..3 (so keys 0..4), like upstream.
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] DOCKING
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Internal Types
|
|
-- Docking: Forward Declarations
|
|
-- Docking: ImGuiDockContext
|
|
-- Docking: ImGuiDockContext Docking/Undocking functions
|
|
-- Docking: ImGuiDockNode
|
|
-- Docking: ImGuiDockNode Tree manipulation functions
|
|
-- Docking: Public Functions (SetWindowDock, DockSpace, DockSpaceOverViewport)
|
|
-- Docking: Builder Functions
|
|
-- Docking: Begin/End Support Functions (called from Begin/End)
|
|
-- Docking: Settings
|
|
-------------------------------------------------------------------------------
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Typical Docking call flow: (root level is generally public API):
|
|
-------------------------------------------------------------------------------
|
|
-- - NewFrame() new dear imgui frame
|
|
-- | DockContextNewFrameUpdateUndocking() - process queued undocking requests
|
|
-- | - DockContextProcessUndockWindow() - process one window undocking request
|
|
-- | - DockContextProcessUndockNode() - process one whole node undocking request
|
|
-- | DockContextNewFrameUpdateUndocking() - process queue docking requests, create floating dock nodes
|
|
-- | - update g.HoveredDockNode - [debug] update node hovered by mouse
|
|
-- | - DockContextProcessDock() - process one docking request
|
|
-- | - DockNodeUpdate()
|
|
-- | - DockNodeUpdateForRootNode()
|
|
-- | - DockNodeUpdateFlagsAndCollapse()
|
|
-- | - DockNodeFindInfo()
|
|
-- | - destroy unused node or tab bar
|
|
-- | - create dock node host window
|
|
-- | - Begin() etc.
|
|
-- | - DockNodeStartMouseMovingWindow()
|
|
-- | - DockNodeTreeUpdatePosSize()
|
|
-- | - DockNodeTreeUpdateSplitter()
|
|
-- | - draw node background
|
|
-- | - DockNodeUpdateTabBar() - create/update tab bar for a docking node
|
|
-- | - DockNodeAddTabBar()
|
|
-- | - DockNodeWindowMenuUpdate()
|
|
-- | - DockNodeCalcTabBarLayout()
|
|
-- | - BeginTabBarEx()
|
|
-- | - TabItemEx() calls
|
|
-- | - EndTabBar()
|
|
-- | - BeginDockableDragDropTarget()
|
|
-- | - DockNodeUpdate() - recurse into child nodes...
|
|
-------------------------------------------------------------------------------
|
|
-- - DockSpace() user submit a dockspace into a window
|
|
-- | Begin(Child) - create a child window
|
|
-- | DockNodeUpdate() - call main dock node update function
|
|
-- | End(Child)
|
|
-- | ItemSize()
|
|
-------------------------------------------------------------------------------
|
|
-- - Begin()
|
|
-- | BeginDocked()
|
|
-- | BeginDockableDragDropSource()
|
|
-- | BeginDockableDragDropTarget()
|
|
-- | - DockNodePreviewDockRender()
|
|
-------------------------------------------------------------------------------
|
|
-- - EndFrame()
|
|
-- | DockContextEndFrame()
|
|
-------------------------------------------------------------------------------
|
|
|
|
local MT = ImGui.GetMetatables()
|
|
|
|
IMGUI_PAYLOAD_TYPE_WINDOW = "_IMWINDOW" -- Payload == ImGuiWindow
|
|
|
|
local DOCKING_HOST_DRAW_CHANNEL_BG = 0 -- Dock host: background fill
|
|
local DOCKING_HOST_DRAW_CHANNEL_FG = 1 -- Dock host: decorations and contents
|
|
|
|
local DOCKING_SPLITTER_SIZE = 2.0
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: flags & enums
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Flags for ImGui.DockSpace(), shared/inherited by child nodes.
|
|
-- (Some flags can be applied to individual nodes directly)
|
|
-- FIXME-DOCK: Also see ImGuiDockNodeFlagsPrivate_ which may involve using the WIP and internal DockBuilder api.
|
|
ImGuiDockNodeFlags = {
|
|
None = 0,
|
|
KeepAliveOnly = bit32.lshift(1, 0), -- -- Don't display the dockspace node but keep it alive. Windows docked into this dockspace node won't be undocked.
|
|
-- NoCentralNode = bit32.lshift(1, 1), -- -- Disable Central Node (the node which can stay empty)
|
|
NoDockingOverCentralNode = bit32.lshift(1, 2), -- -- Disable docking over the Central Node, which will be always kept empty.
|
|
PassthruCentralNode = bit32.lshift(1, 3), -- -- Enable passthru dockspace: 1) DockSpace() will render a ImGuiCol.WindowBg background covering everything excepted the Central Node when empty. Meaning the host window should probably use SetNextWindowBgAlpha(0.0) prior to Begin() when using this. 2) When Central Node is empty: let inputs pass-through + won't display a DockingEmptyBg background. See demo for details.
|
|
NoDockingSplit = bit32.lshift(1, 4), -- -- Disable other windows/nodes from splitting this node.
|
|
NoResize = bit32.lshift(1, 5), -- Saved -- Disable resizing node using the splitter/separators. Useful with programmatically setup dockspaces.
|
|
AutoHideTabBar = bit32.lshift(1, 6), -- -- Tab bar will automatically hide when there is a single window in the dock node.
|
|
NoUndocking = bit32.lshift(1, 7), -- -- Disable undocking this node.
|
|
|
|
-- Extend ImGuiDockNodeFlags_ (ImGuiDockNodeFlagsPrivate_)
|
|
-- [Internal]
|
|
DockSpace = bit32.lshift(1, 10), -- Saved -- A dockspace is a node that occupy space within an existing user window. Otherwise the node is floating and create its own window.
|
|
CentralNode = bit32.lshift(1, 11), -- Saved -- The central node has 2 main properties: stay visible when empty, only use "remaining" spaces from its neighbor.
|
|
NoTabBar = bit32.lshift(1, 12), -- Saved -- Tab bar is completely unavailable. No triangle in the corner to enable it back.
|
|
HiddenTabBar = bit32.lshift(1, 13), -- Saved -- Tab bar is hidden, with a triangle in the corner to show it again (NB: actual tab-bar instance may be destroyed as this is only used for single-window tab bar)
|
|
NoWindowMenuButton = bit32.lshift(1, 14), -- Saved -- Disable window/docking menu (that one that appears instead of the collapse button)
|
|
NoCloseButton = bit32.lshift(1, 15), -- Saved -- Disable close button
|
|
NoResizeX = bit32.lshift(1, 16), -- --
|
|
NoResizeY = bit32.lshift(1, 17), -- --
|
|
DockedWindowsInFocusRoute = bit32.lshift(1, 18), -- -- Any docked window will be automatically be focus-route chained (window.ParentWindowForFocusRoute set to this) so Shortcut() in this window can run when any docked window is focused.
|
|
|
|
-- Disable docking/undocking actions in this dockspace or individual node (existing docked nodes will be preserved)
|
|
-- Those are not exposed in public because the desirable sharing/inheriting/copy-flag-on-split behaviors are quite difficult to design and understand.
|
|
-- The two public flags ImGuiDockNodeFlags.NoDockingOverCentralNode/ImGuiDockNodeFlags.NoDockingSplit don't have those issues.
|
|
NoDockingSplitOther = bit32.lshift(1, 19), -- -- Disable this node from splitting other windows/nodes.
|
|
NoDockingOverMe = bit32.lshift(1, 20), -- -- Disable other windows/nodes from being docked over this node.
|
|
NoDockingOverOther = bit32.lshift(1, 21), -- -- Disable this node from being docked over another window or non-empty node.
|
|
NoDockingOverEmpty = bit32.lshift(1, 22), -- -- Disable this node from being docked over an empty node (e.g. DockSpace with no other windows)
|
|
}
|
|
do
|
|
local F = ImGuiDockNodeFlags
|
|
F.NoSplit = F.NoDockingSplit -- Renamed in 1.90
|
|
F.NoDockingInCentralNode = F.NoDockingOverCentralNode -- Renamed in 1.90
|
|
F.NoDocking = bit32.bor(F.NoDockingOverMe, F.NoDockingOverOther, F.NoDockingOverEmpty, F.NoDockingSplit, F.NoDockingSplitOther)
|
|
-- Masks
|
|
F.SharedFlagsInheritMask_ = 0xFFFFFFFF
|
|
F.NoResizeFlagsMask_ = bit32.bor(F.NoResize, F.NoResizeX, F.NoResizeY)
|
|
-- When splitting, those local flags are moved to the inheriting child, never duplicated
|
|
F.LocalFlagsTransferMask_ = bit32.bor(F.NoDockingSplit, F.NoResizeFlagsMask_, F.AutoHideTabBar, F.CentralNode, F.NoTabBar, F.HiddenTabBar, F.NoWindowMenuButton, F.NoCloseButton)
|
|
F.SavedFlagsMask_ = bit32.bor(F.NoResizeFlagsMask_, F.DockSpace, F.CentralNode, F.NoTabBar, F.HiddenTabBar, F.NoWindowMenuButton, F.NoCloseButton)
|
|
end
|
|
|
|
-- Store the source authority (dock node vs window) of a field
|
|
ImGuiDataAuthority = { Auto = 0, DockNode = 1, Window = 2 }
|
|
|
|
ImGuiDockNodeState = {
|
|
Unknown = 0,
|
|
HostWindowHiddenBecauseSingleWindow = 1,
|
|
HostWindowHiddenBecauseWindowsAreResizing = 2,
|
|
HostWindowVisible = 3,
|
|
}
|
|
|
|
-- List of colors that are stored at the time of Begin() into Docked Windows.
|
|
-- We currently store the packed colors in a simple array window.DockStyle.Colors[].
|
|
-- A better solution may involve appending into a log of colors in ImGuiContext + store offsets into those arrays in ImGuiWindow,
|
|
-- but it would be more complex as we'd need to double-buffer both as e.g. drop target may refer to window from last frame.
|
|
ImGuiWindowDockStyleCol = {
|
|
Text = 0, TabHovered = 1, TabFocused = 2, TabSelected = 3, TabSelectedOverline = 4,
|
|
TabDimmed = 5, TabDimmedSelected = 6, TabDimmedSelectedOverline = 7, UnsavedMarker = 8, COUNT = 9,
|
|
}
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Internal Types
|
|
-------------------------------------------------------------------------------
|
|
-- - ImGuiDockRequestType
|
|
-- - ImGuiDockRequest
|
|
-- - ImGuiDockPreviewData
|
|
-- - ImGuiDockNodeSettings
|
|
-- - ImGuiDockContext
|
|
-------------------------------------------------------------------------------
|
|
|
|
local ImGuiDockRequestType = { None = 0, Dock = 1, Undock = 2, Split = 3 } -- Split is the same as Dock but without a DockPayload
|
|
|
|
local function ImGuiDockRequest()
|
|
return {
|
|
Type = ImGuiDockRequestType.None,
|
|
-- DockTargetWindow: Destination/Target Window to dock into (may be a loose window or a DockNode, might be nil in which case DockTargetNode cannot be nil)
|
|
-- DockTargetNode: Destination/Target Node to dock into
|
|
-- DockPayload: Source/Payload window to dock (may be a loose window or a DockNode), [Optional]
|
|
DockTargetWindow = nil, DockTargetNode = nil, DockPayload = nil,
|
|
DockSplitDir = ImGuiDir.None, DockSplitRatio = 0.5, DockSplitOuter = false,
|
|
UndockTargetWindow = nil, UndockTargetNode = nil,
|
|
}
|
|
end
|
|
|
|
-- Persistent Settings data, stored contiguously in SettingsNodes
|
|
local function ImGuiDockNodeSettings()
|
|
return {
|
|
ID = 0, ParentNodeId = 0, ParentWindowId = 0, SelectedTabId = 0,
|
|
SplitAxis = ImGuiAxis.None, Depth = 0, Flags = 0, -- Flags: NB: We save individual flags one by one in ascii format (ImGuiDockNodeFlags.SavedFlagsMask_)
|
|
Pos = ImVec2(0, 0), Size = ImVec2(0, 0), SizeRef = ImVec2(0, 0),
|
|
}
|
|
end
|
|
|
|
function ImGuiDockContext()
|
|
-- Nodes: Map ID -> ImGuiDockNode: Active nodes
|
|
return { Nodes = {}, Requests = ImVector(), NodesSettings = ImVector(), WantFullRebuild = false }
|
|
end
|
|
|
|
MT.ImGuiDockNode = {}
|
|
MT.ImGuiDockNode.__index = MT.ImGuiDockNode
|
|
local DN = MT.ImGuiDockNode
|
|
|
|
function ImGuiDockNode(id)
|
|
return setmetatable({
|
|
ID = id,
|
|
-- SharedFlags: (Write) Flags shared by all nodes of a same dockspace hierarchy (inherited from the root node)
|
|
-- LocalFlags: (Write) Flags specific to this node
|
|
-- LocalFlagsInWindows: (Write) Flags specific to this node, applied from windows
|
|
-- MergedFlags: (Read) Effective flags (== SharedFlags | LocalFlagsInNode | LocalFlagsInWindows)
|
|
SharedFlags = 0, LocalFlags = 0, LocalFlagsInWindows = 0, MergedFlags = 0,
|
|
State = ImGuiDockNodeState.Unknown,
|
|
ParentNode = nil,
|
|
ChildNodes = { [0] = nil, [1] = nil }, -- [Split node only] Child nodes (left/right or top/bottom). Consider switching to an array.
|
|
Windows = ImVector(), -- Note: unordered list! Iterate TabBar.Tabs for user-order.
|
|
TabBar = nil,
|
|
-- Pos: Current position
|
|
-- Size: Current size
|
|
-- SizeRef: [Split node only] Last explicitly written-to size (overridden when using a splitter affecting the node), used to calculate Size.
|
|
Pos = ImVec2(0, 0), Size = ImVec2(0, 0), SizeRef = ImVec2(0, 0),
|
|
SplitAxis = ImGuiAxis.None, -- [Split node only] Split axis (X or Y)
|
|
LastBgColor = IM_COL32_WHITE,
|
|
WindowClass = ImGuiWindowClass(), -- [Root node only]
|
|
-- VisibleWindow: Generally point to window which is ID is == SelectedTabID, but when CTRL+Tabbing this can be a different window.
|
|
HostWindow = nil, VisibleWindow = nil,
|
|
-- CentralNode: [Root node only] Pointer to central node.
|
|
-- OnlyNodeWithWindows: [Root node only] Set when there is a single visible node within the hierarchy.
|
|
CentralNode = nil, OnlyNodeWithWindows = nil,
|
|
CountNodeWithWindows = 0, -- [Root node only]
|
|
-- LastFrameAlive: Last frame number the node was updated or kept alive explicitly with DockSpace() + ImGuiDockNodeFlags.KeepAliveOnly
|
|
-- LastFrameActive: Last frame number the node was updated.
|
|
-- LastFrameFocused: Last frame number the node was focused.
|
|
LastFrameAlive = -1, LastFrameActive = -1, LastFrameFocused = -1,
|
|
-- LastFocusedNodeId: [Root node only] Which of our child docking node (any ancestor in the hierarchy) was last focused.
|
|
-- SelectedTabId: [Leaf node only] Which of our tab/window is selected.
|
|
-- WantCloseTabId: [Leaf node only] Set when closing a specific tab/window.
|
|
-- RefViewportId: Reference viewport ID from visible window when HostWindow == nil.
|
|
LastFocusedNodeId = 0, SelectedTabId = 0, WantCloseTabId = 0, RefViewportId = 0,
|
|
AuthorityForPos = ImGuiDataAuthority.DockNode, AuthorityForSize = ImGuiDataAuthority.DockNode, -- ImGuiDataAuthority
|
|
AuthorityForViewport = ImGuiDataAuthority.Auto,
|
|
IsVisible = true, IsFocused = false, IsBgDrawnThisFrame = false, -- IsVisible: Set to false when the node is hidden (usually disabled as it has no active window)
|
|
HasCloseButton = false, HasWindowMenuButton = false, HasCentralNodeChild = false, -- HasCloseButton: Provide space for a close button (if any of the docked window has one). Note that button may be hidden on window without one.
|
|
-- WantCloseAll: Set when closing all tabs at once.
|
|
-- WantMouseMove: After a node extraction we need to transition toward moving the newly created host window
|
|
WantCloseAll = false, WantLockSizeOnce = false, WantMouseMove = false,
|
|
WantHiddenTabBarUpdate = false, WantHiddenTabBarToggle = false,
|
|
}, DN)
|
|
end
|
|
|
|
function DN:IsRootNode() return self.ParentNode == nil end
|
|
function DN:IsDockSpace() return bit32.band(self.MergedFlags, ImGuiDockNodeFlags.DockSpace) ~= 0 end
|
|
function DN:IsFloatingNode() return self.ParentNode == nil and bit32.band(self.MergedFlags, ImGuiDockNodeFlags.DockSpace) == 0 end
|
|
function DN:IsCentralNode() return bit32.band(self.MergedFlags, ImGuiDockNodeFlags.CentralNode) ~= 0 end
|
|
function DN:IsHiddenTabBar() return bit32.band(self.MergedFlags, ImGuiDockNodeFlags.HiddenTabBar) ~= 0 end -- Hidden tab bar can be shown back by clicking the small triangle
|
|
function DN:IsNoTabBar() return bit32.band(self.MergedFlags, ImGuiDockNodeFlags.NoTabBar) ~= 0 end -- Never show a tab bar
|
|
function DN:IsSplitNode() return self.ChildNodes[0] ~= nil end
|
|
function DN:IsLeafNode() return self.ChildNodes[0] == nil end
|
|
function DN:IsEmpty() return self.ChildNodes[0] == nil and self.Windows.Size == 0 end
|
|
function DN:Rect() return ImRect(self.Pos.x, self.Pos.y, self.Pos.x + self.Size.x, self.Pos.y + self.Size.y) end
|
|
function DN:SetLocalFlags(flags) self.LocalFlags = flags; self:UpdateMergedFlags() end
|
|
function DN:UpdateMergedFlags() self.MergedFlags = bit32.bor(self.SharedFlags, self.LocalFlags, self.LocalFlagsInWindows) end
|
|
|
|
local function ImGuiDockPreviewData()
|
|
local d = {
|
|
FutureNode = ImGuiDockNode(0),
|
|
-- IsSidesAvailable: Hold your breath, grammar freaks..
|
|
-- IsSplitDirExplicit: Set when hovered the drop rect (vs. implicit SplitDir==None when hovered the window)
|
|
IsDropAllowed = false, IsCenterAvailable = false, IsSidesAvailable = false, IsSplitDirExplicit = false,
|
|
SplitNode = nil, SplitDir = ImGuiDir.None, SplitRatio = 0.0,
|
|
DropRectsDraw = {}, -- May be slightly different from hit-testing drop rects used in DockNodeCalcDropRects()
|
|
}
|
|
for n = 0, 4 do d.DropRectsDraw[n] = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX) end
|
|
return d
|
|
end
|
|
|
|
local function DockNodeGetHostWindowTitle(node)
|
|
return string.format("##DockNode_%02X", node.ID)
|
|
end
|
|
|
|
local function IsRectInverted(r) return r.Min.x > r.Max.x or r.Min.y > r.Max.y end
|
|
|
|
--- Sorted snapshot of node IDs (upstream iterates ImGuiStorage which is sorted by key)
|
|
local function DockContextNodesSorted(ctx)
|
|
local ids = {}
|
|
for id in pairs(ctx.DockContext.Nodes) do ids[#ids + 1] = id end
|
|
table.sort(ids)
|
|
return ids
|
|
end
|
|
ImGui.DockContextNodesSorted = DockContextNodesSorted
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Forward Declarations
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Settings
|
|
local DockSettingsHandler_ClearAll, DockSettingsHandler_ApplyAll, DockSettingsHandler_ReadOpen, DockSettingsHandler_ReadLine, DockSettingsHandler_WriteAll
|
|
local DockSettingsRenameNodeReferences, DockSettingsRemoveNodeReferences, DockSettingsFindNodeSettings
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: ImGuiDockContext
|
|
-------------------------------------------------------------------------------
|
|
-- The lifetime model is different from the one of regular windows: we always create a ImGuiDockNode for each ImGuiDockNodeSettings,
|
|
-- or we always hold the entire docking node tree. Nodes are frequently hidden, e.g. if the window(s) or child nodes they host are not active.
|
|
-- At boot time only, we run a simple GC to remove nodes that have no references.
|
|
-- Because dock node settings (which are small, contiguous structures) are always mirrored by their corresponding dock nodes (more complete structures),
|
|
-- we can also very easily recreate the nodes from scratch given the settings data (this is what DockContextRebuild() does).
|
|
-- This is convenient as docking reconfiguration can be implemented by mostly poking at the simpler settings data.
|
|
-------------------------------------------------------------------------------
|
|
-- - DockContextInitialize()
|
|
-- - DockContextShutdown()
|
|
-- - DockContextClearNodes()
|
|
-- - DockContextRebuildNodes()
|
|
-- - DockContextNewFrameUpdateUndocking()
|
|
-- - DockContextNewFrameUpdateDocking()
|
|
-- - DockContextEndFrame()
|
|
-- - DockContextFindNodeByID()
|
|
-- - DockContextBindNodeToWindow()
|
|
-- - DockContextGenNodeID()
|
|
-- - DockContextAddNode()
|
|
-- - DockContextRemoveNode()
|
|
-- - ImGuiDockContextPruneNodeData
|
|
-- - DockContextPruneUnusedSettingsNodes()
|
|
-- - DockContextBuildNodesFromSettings()
|
|
-- - DockContextBuildAddWindowsToNodes()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.DockContextInitialize(ctx)
|
|
local g = ctx
|
|
|
|
-- Add .ini handle for persistent docking data
|
|
local ini_handler = ImGuiSettingsHandler()
|
|
ini_handler.TypeName = "Docking"
|
|
ini_handler.TypeHash = ImHashStr("Docking")
|
|
ini_handler.ClearAllFn = DockSettingsHandler_ClearAll
|
|
ini_handler.ReadInitFn = DockSettingsHandler_ClearAll -- Also clear on read
|
|
ini_handler.ReadOpenFn = DockSettingsHandler_ReadOpen
|
|
ini_handler.ReadLineFn = DockSettingsHandler_ReadLine
|
|
ini_handler.ApplyAllFn = DockSettingsHandler_ApplyAll
|
|
ini_handler.WriteAllFn = DockSettingsHandler_WriteAll
|
|
g.SettingsHandlers:push_back(ini_handler)
|
|
|
|
g.DockNodeWindowMenuHandler = ImGui.DockNodeWindowMenuHandler_Default
|
|
end
|
|
|
|
function ImGui.DockContextShutdown(ctx)
|
|
for _, id in ipairs(DockContextNodesSorted(ctx)) do
|
|
local node = ctx.DockContext.Nodes[id]
|
|
if node then ImGui.DockContextDeleteNode(ctx, node) end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockContextClearNodes(ctx, root_id, clear_settings_refs)
|
|
ImGui.DockBuilderRemoveNodeDockedWindows(root_id, clear_settings_refs)
|
|
ImGui.DockBuilderRemoveNodeChildNodes(root_id)
|
|
end
|
|
|
|
-- [DEBUG] This function also acts as a defacto test to make sure we can rebuild from scratch without a glitch
|
|
-- (Different from DockSettingsHandler_ClearAll() + DockSettingsHandler_ApplyAll() because this reuses current settings!)
|
|
function ImGui.DockContextRebuildNodes(ctx)
|
|
local dc = ctx.DockContext
|
|
ImGui.SaveIniSettingsToMemory()
|
|
local root_id = 0 -- Rebuild all
|
|
ImGui.DockContextClearNodes(ctx, root_id, false)
|
|
ImGui.DockContextBuildNodesFromSettings(ctx, dc.NodesSettings.Data, dc.NodesSettings.Size)
|
|
ImGui.DockContextBuildAddWindowsToNodes(ctx, root_id)
|
|
end
|
|
|
|
-- Docking context update function, called by NewFrame()
|
|
function ImGui.DockContextNewFrameUpdateUndocking(ctx)
|
|
local g = ctx
|
|
local dc = ctx.DockContext
|
|
if bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.DockingEnable) == 0 then
|
|
if next(dc.Nodes) ~= nil or dc.Requests.Size > 0 then
|
|
ImGui.DockContextClearNodes(ctx, 0, true)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Setting NoSplit at runtime merges all nodes
|
|
if g.IO.ConfigDockingNoSplit then
|
|
for _, id in ipairs(DockContextNodesSorted(ctx)) do
|
|
local node = dc.Nodes[id]
|
|
if node and node:IsRootNode() and node:IsSplitNode() then
|
|
ImGui.DockBuilderRemoveNodeChildNodes(node.ID)
|
|
-- dc.WantFullRebuild = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Process full rebuild
|
|
if dc.WantFullRebuild then
|
|
ImGui.DockContextRebuildNodes(ctx)
|
|
dc.WantFullRebuild = false
|
|
end
|
|
|
|
-- Process Undocking requests (we need to process them _before_ the UpdateMouseMovingWindowNewFrame call in NewFrame)
|
|
for _, req in dc.Requests:iter() do
|
|
if req.Type == ImGuiDockRequestType.Undock and req.UndockTargetWindow then
|
|
ImGui.DockContextProcessUndockWindow(ctx, req.UndockTargetWindow)
|
|
elseif req.Type == ImGuiDockRequestType.Undock and req.UndockTargetNode then
|
|
ImGui.DockContextProcessUndockNode(ctx, req.UndockTargetNode)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Docking context update function, called by NewFrame()
|
|
function ImGui.DockContextNewFrameUpdateDocking(ctx)
|
|
local g = ctx
|
|
local dc = ctx.DockContext
|
|
if bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.DockingEnable) == 0 then return end
|
|
|
|
-- [DEBUG] Store hovered dock node.
|
|
-- We could in theory use DockNodeTreeFindVisibleNodeByPos() on the root host dock node, but using .DockNode is a good shortcut.
|
|
-- Note this is mostly a debug thing and isn't actually used for docking target, because docking involve more detailed filtering.
|
|
g.DebugHoveredDockNode = nil
|
|
local hovered_window = g.HoveredWindowUnderMovingWindow
|
|
if hovered_window then
|
|
if hovered_window.DockNodeAsHost then
|
|
g.DebugHoveredDockNode = ImGui.DockNodeTreeFindVisibleNodeByPos(hovered_window.DockNodeAsHost, g.IO.MousePos)
|
|
elseif hovered_window.RootWindow.DockNode then
|
|
g.DebugHoveredDockNode = hovered_window.RootWindow.DockNode
|
|
end
|
|
end
|
|
|
|
-- Process Docking requests
|
|
for _, req in dc.Requests:iter() do
|
|
if req.Type == ImGuiDockRequestType.Dock then
|
|
ImGui.DockContextProcessDock(ctx, req)
|
|
end
|
|
end
|
|
dc.Requests:resize(0)
|
|
|
|
-- Create windows for each automatic docking nodes
|
|
for _, id in ipairs(DockContextNodesSorted(ctx)) do
|
|
local node = dc.Nodes[id]
|
|
if node and node:IsFloatingNode() then
|
|
ImGui.DockNodeUpdate(node)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockContextEndFrame(ctx)
|
|
-- Draw backgrounds of node missing their window
|
|
local g = ctx
|
|
local dc = g.DockContext
|
|
for _, id in ipairs(DockContextNodesSorted(ctx)) do
|
|
local node = dc.Nodes[id]
|
|
if node and node.LastFrameActive == g.FrameCount and node.IsVisible and node.HostWindow and node:IsLeafNode() and not node.IsBgDrawnThisFrame then
|
|
local bg_rect = ImRect(node.Pos + ImVec2(0.0, ImGui.GetFrameHeight()), node.Pos + node.Size)
|
|
local bg_rounding_flags = ImGui.CalcRoundingFlagsForRectInRect(bg_rect, node.HostWindow:Rect(), g.Style.DockingSeparatorSize)
|
|
node.HostWindow.DrawList:ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG)
|
|
node.HostWindow.DrawList:AddRectFilled(bg_rect.Min, bg_rect.Max, node.LastBgColor, node.HostWindow.WindowRounding, bg_rounding_flags)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockContextFindNodeByID(ctx, id)
|
|
return ctx.DockContext.Nodes[id]
|
|
end
|
|
|
|
function ImGui.DockContextGenNodeID(ctx)
|
|
-- Generate an ID for new node (the exact ID value doesn't matter as long as it is not already used)
|
|
-- FIXME-OPT FIXME-DOCK: This is suboptimal, even if the node count is small enough not to be a worry.0
|
|
-- We should poke in ctx.Nodes to find a suitable ID faster. Even more so trivial that ctx.Nodes lookup is already sorted.
|
|
local id = 0x0001
|
|
while ImGui.DockContextFindNodeByID(ctx, id) ~= nil do id = id + 1 end
|
|
return id
|
|
end
|
|
|
|
function ImGui.DockContextAddNode(ctx, id)
|
|
-- Generate an ID for the new node (the exact ID value doesn't matter as long as it is not already used) and add the first window.
|
|
if id == 0 then
|
|
id = ImGui.DockContextGenNodeID(ctx)
|
|
else
|
|
IM_ASSERT(ImGui.DockContextFindNodeByID(ctx, id) == nil)
|
|
end
|
|
|
|
-- We don't set node.LastFrameAlive on construction. Nodes are always created at all time to reflect .ini settings!
|
|
local node = ImGuiDockNode(id)
|
|
ctx.DockContext.Nodes[node.ID] = node
|
|
return node
|
|
end
|
|
|
|
function ImGui.DockContextRemoveNode(ctx, node, merge_sibling_into_parent_node)
|
|
IM_ASSERT(ImGui.DockContextFindNodeByID(ctx, node.ID) == node)
|
|
IM_ASSERT(node.ChildNodes[0] == nil and node.ChildNodes[1] == nil)
|
|
IM_ASSERT(node.Windows.Size == 0)
|
|
|
|
if node.HostWindow then node.HostWindow.DockNodeAsHost = nil end
|
|
|
|
local parent_node = node.ParentNode
|
|
local merge = (merge_sibling_into_parent_node and parent_node ~= nil)
|
|
if merge then
|
|
IM_ASSERT(parent_node.ChildNodes[0] == node or parent_node.ChildNodes[1] == node)
|
|
local sibling_node = (parent_node.ChildNodes[0] == node) and parent_node.ChildNodes[1] or parent_node.ChildNodes[0]
|
|
ImGui.DockNodeTreeMerge(ctx, parent_node, sibling_node)
|
|
else
|
|
if parent_node then
|
|
for n = 0, 1 do
|
|
if parent_node.ChildNodes[n] == node then node.ParentNode.ChildNodes[n] = nil end
|
|
end
|
|
end
|
|
ImGui.DockContextDeleteNode(ctx, node)
|
|
end
|
|
end
|
|
|
|
-- Raw-ish delete
|
|
function ImGui.DockContextDeleteNode(ctx, node)
|
|
local dc = ctx.DockContext
|
|
node.TabBar = nil
|
|
dc.Nodes[node.ID] = nil
|
|
end
|
|
|
|
-- Garbage collect unused nodes (run once at init time)
|
|
function ImGui.DockContextPruneUnusedSettingsNodes(ctx)
|
|
local g = ctx
|
|
local dc = ctx.DockContext
|
|
IM_ASSERT(g.Windows.Size == 0)
|
|
|
|
local pool = {}
|
|
local function GetOrAdd(id)
|
|
local d = pool[id]
|
|
if not d then d = { CountWindows = 0, CountChildWindows = 0, CountChildNodes = 0, RootId = 0 }; pool[id] = d end
|
|
return d
|
|
end
|
|
|
|
-- Count child nodes and compute RootID
|
|
for _, settings in dc.NodesSettings:iter() do
|
|
if pool[settings.ID] ~= nil then
|
|
settings.ID = 0 -- Duplicate
|
|
else
|
|
local parent_data = (settings.ParentNodeId ~= 0) and pool[settings.ParentNodeId] or nil
|
|
GetOrAdd(settings.ID).RootId = parent_data and parent_data.RootId or settings.ID
|
|
if settings.ParentNodeId ~= 0 then
|
|
local p = GetOrAdd(settings.ParentNodeId)
|
|
p.CountChildNodes = p.CountChildNodes + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Count reference to dock ids from dockspaces
|
|
-- We track the 'auto-DockNode <- manual-Window <- manual-DockSpace' in order to avoid 'auto-DockNode' being ditched by DockContextPruneUnusedSettingsNodes()
|
|
for _, settings in dc.NodesSettings:iter() do
|
|
if settings.ParentWindowId ~= 0 then
|
|
local window_settings = ImGui.FindWindowSettingsByID(settings.ParentWindowId)
|
|
if window_settings and window_settings.DockId ~= 0 then
|
|
local data = pool[window_settings.DockId]
|
|
if data then data.CountChildNodes = data.CountChildNodes + 1 end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Count reference to dock ids from window settings
|
|
-- We guard against the possibility of an invalid .ini file (RootID may point to a missing node)
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
local dock_id = settings.DockId
|
|
if dock_id and dock_id ~= 0 then
|
|
local data = pool[dock_id]
|
|
if data then
|
|
data.CountWindows = data.CountWindows + 1
|
|
local data_root = (data.RootId == dock_id) and data or pool[data.RootId]
|
|
if data_root then data_root.CountChildWindows = data_root.CountChildWindows + 1 end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Prune
|
|
for _, settings in dc.NodesSettings:iter() do
|
|
local data = pool[settings.ID]
|
|
if data ~= nil and data.CountWindows <= 1 then
|
|
local data_root = (settings.ID == data.RootId) and data or pool[data.RootId]
|
|
local data_parent = (settings.ParentNodeId ~= 0) and pool[settings.ParentNodeId] or nil
|
|
|
|
local remove = false
|
|
remove = remove or (data.CountWindows == 1 and settings.ParentNodeId == 0 and data.CountChildNodes == 0 and bit32.band(settings.Flags, ImGuiDockNodeFlags.CentralNode) == 0) -- Floating root node with only 1 window
|
|
remove = remove or (data.CountWindows == 0 and settings.ParentNodeId == 0 and data.CountChildNodes == 0) -- Leaf nodes with 0 window
|
|
remove = remove or (data_root == nil or data_root.CountChildWindows == 0)
|
|
if remove then
|
|
DockSettingsRemoveNodeReferences({ settings.ID }, 1)
|
|
settings.ID = 0
|
|
elseif data_parent and data_parent.CountChildNodes == 1 then
|
|
DockSettingsRenameNodeReferences(settings.ID, settings.ParentNodeId)
|
|
settings.ID = 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param node_settings_array table # 1-based array
|
|
function ImGui.DockContextBuildNodesFromSettings(ctx, node_settings_array, node_settings_count)
|
|
-- Build nodes
|
|
for node_n = 1, node_settings_count do
|
|
local settings = node_settings_array[node_n]
|
|
if settings.ID ~= 0 and ImGui.DockContextFindNodeByID(ctx, settings.ID) == nil then
|
|
local node = ImGui.DockContextAddNode(ctx, settings.ID)
|
|
node.ParentNode = (settings.ParentNodeId ~= 0) and ImGui.DockContextFindNodeByID(ctx, settings.ParentNodeId) or nil
|
|
node.Pos = ImVec2(settings.Pos.x, settings.Pos.y)
|
|
node.Size = ImVec2(settings.Size.x, settings.Size.y)
|
|
node.SizeRef = ImVec2(settings.SizeRef.x, settings.SizeRef.y)
|
|
node.AuthorityForPos = ImGuiDataAuthority.DockNode
|
|
node.AuthorityForSize = ImGuiDataAuthority.DockNode
|
|
node.AuthorityForViewport = ImGuiDataAuthority.DockNode
|
|
if node.ParentNode and node.ParentNode.ChildNodes[0] == nil then
|
|
node.ParentNode.ChildNodes[0] = node
|
|
elseif node.ParentNode and node.ParentNode.ChildNodes[1] == nil then
|
|
node.ParentNode.ChildNodes[1] = node
|
|
end
|
|
node.SelectedTabId = settings.SelectedTabId
|
|
node.SplitAxis = settings.SplitAxis
|
|
node:SetLocalFlags(bit32.band(settings.Flags, ImGuiDockNodeFlags.SavedFlagsMask_))
|
|
|
|
-- Bind host window immediately if it already exist (in case of a rebuild)
|
|
-- This is useful as the RootWindowForTitleBarHighlight links necessary to highlight the currently focused node requires node.HostWindow to be set.
|
|
local root_node = ImGui.DockNodeGetRootNode(node)
|
|
node.HostWindow = ImGui.FindWindowByName(DockNodeGetHostWindowTitle(root_node))
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockContextBuildAddWindowsToNodes(ctx, root_id)
|
|
-- Rebind all windows to nodes (they can also lazily rebind but we'll have a visible glitch during the first frame)
|
|
local g = ctx
|
|
for _, window in g.Windows:iter() do
|
|
if window.DockId ~= 0 and window.LastFrameActive >= g.FrameCount - 1 and window.DockNode == nil then
|
|
local node = ImGui.DockContextFindNodeByID(ctx, window.DockId)
|
|
IM_ASSERT(node ~= nil) -- This should have been called after DockContextBuildNodesFromSettings()
|
|
if root_id == 0 or ImGui.DockNodeGetRootNode(node).ID == root_id then
|
|
ImGui.DockNodeAddWindow(node, window, true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: ImGuiDockContext Docking/Undocking functions
|
|
-------------------------------------------------------------------------------
|
|
-- - DockContextQueueDock()
|
|
-- - DockContextQueueUndockWindow()
|
|
-- - DockContextQueueUndockNode()
|
|
-- - DockContextQueueNotifyRemovedNode()
|
|
-- - DockContextProcessDock()
|
|
-- - DockContextProcessUndockWindow()
|
|
-- - DockContextProcessUndockNode()
|
|
-- - DockContextCalcDropPosForDocking()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.DockContextQueueDock(ctx, target, target_node, payload, split_dir, split_ratio, split_outer)
|
|
IM_ASSERT(target ~= payload)
|
|
local req = ImGuiDockRequest()
|
|
req.Type = ImGuiDockRequestType.Dock
|
|
req.DockTargetWindow = target
|
|
req.DockTargetNode = target_node
|
|
req.DockPayload = payload
|
|
req.DockSplitDir = split_dir
|
|
req.DockSplitRatio = split_ratio
|
|
req.DockSplitOuter = split_outer
|
|
ctx.DockContext.Requests:push_back(req)
|
|
end
|
|
|
|
function ImGui.DockContextQueueUndockWindow(ctx, window)
|
|
local req = ImGuiDockRequest()
|
|
req.Type = ImGuiDockRequestType.Undock
|
|
req.UndockTargetWindow = window
|
|
ctx.DockContext.Requests:push_back(req)
|
|
end
|
|
|
|
function ImGui.DockContextQueueUndockNode(ctx, node)
|
|
local req = ImGuiDockRequest()
|
|
req.Type = ImGuiDockRequestType.Undock
|
|
req.UndockTargetNode = node
|
|
ctx.DockContext.Requests:push_back(req)
|
|
end
|
|
|
|
function ImGui.DockContextQueueNotifyRemovedNode(ctx, node)
|
|
for _, req in ctx.DockContext.Requests:iter() do
|
|
if req.DockTargetNode == node then req.Type = ImGuiDockRequestType.None end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockContextProcessDock(ctx, req)
|
|
IM_ASSERT((req.Type == ImGuiDockRequestType.Dock and req.DockPayload ~= nil) or (req.Type == ImGuiDockRequestType.Split and req.DockPayload == nil))
|
|
IM_ASSERT(req.DockTargetWindow ~= nil or req.DockTargetNode ~= nil)
|
|
local g = ctx
|
|
|
|
local payload_window = req.DockPayload -- Optional
|
|
local target_window = req.DockTargetWindow
|
|
local node = req.DockTargetNode
|
|
|
|
-- Decide which Tab will be selected at the end of the operation
|
|
local next_selected_id = 0
|
|
local payload_node = nil
|
|
if payload_window then
|
|
payload_node = payload_window.DockNodeAsHost
|
|
payload_window.DockNodeAsHost = nil -- Important to clear this as the node will have its life as a child which might be merged/deleted later.
|
|
if payload_node and payload_node:IsLeafNode() then
|
|
next_selected_id = (payload_node.TabBar.NextSelectedTabId ~= 0) and payload_node.TabBar.NextSelectedTabId or payload_node.TabBar.SelectedTabId
|
|
end
|
|
if payload_node == nil then next_selected_id = payload_window.TabId end
|
|
end
|
|
|
|
-- FIXME-DOCK: When we are trying to dock an existing single-window node into a loose window, transfer Node ID as well
|
|
-- When processing an interactive split, usually LastFrameAlive will be < g.FrameCount. But DockBuilder operations can make it ==.
|
|
if node then IM_ASSERT(node.LastFrameAlive <= g.FrameCount) end
|
|
if node and target_window and node == target_window.DockNodeAsHost then
|
|
IM_ASSERT(node.Windows.Size > 0 or node:IsSplitNode() or node:IsCentralNode())
|
|
end
|
|
|
|
-- Create new node and add existing window to it
|
|
if node == nil then
|
|
node = ImGui.DockContextAddNode(ctx, 0)
|
|
node.Pos = ImVec2(target_window.Pos.x, target_window.Pos.y)
|
|
node.Size = ImVec2(target_window.Size.x, target_window.Size.y)
|
|
if target_window.DockNodeAsHost == nil then
|
|
ImGui.DockNodeAddWindow(node, target_window, true)
|
|
local t0 = node.TabBar.Tabs.Data[1]
|
|
t0.Flags = bit32.band(t0.Flags, bit32.bnot(ImGuiTabItemFlags.Unsorted))
|
|
target_window.DockIsActive = true
|
|
end
|
|
end
|
|
|
|
local split_dir = req.DockSplitDir
|
|
if split_dir ~= ImGuiDir.None then
|
|
-- Split into two, one side will be our payload node unless we are dropping a loose window
|
|
local split_axis = (split_dir == ImGuiDir.Left or split_dir == ImGuiDir.Right) and ImGuiAxis.X or ImGuiAxis.Y
|
|
local split_inheritor_child_idx = (split_dir == ImGuiDir.Left or split_dir == ImGuiDir.Up) and 1 or 0 -- Current contents will be moved to the opposite side
|
|
local split_ratio = req.DockSplitRatio
|
|
ImGui.DockNodeTreeSplit(ctx, node, split_axis, split_inheritor_child_idx, split_ratio, payload_node) -- payload_node may be nil here!
|
|
local new_node = node.ChildNodes[bit32.bxor(split_inheritor_child_idx, 1)]
|
|
new_node.HostWindow = node.HostWindow
|
|
node = new_node
|
|
end
|
|
node:SetLocalFlags(bit32.band(node.LocalFlags, bit32.bnot(ImGuiDockNodeFlags.HiddenTabBar)))
|
|
|
|
if node ~= payload_node then
|
|
-- Create tab bar before we call DockNodeMoveWindows (which would attempt to move the old tab-bar, which would lead us to payload tabs wrongly appearing before target tabs!)
|
|
if node.Windows.Size > 0 and node.TabBar == nil then
|
|
ImGui.DockNodeAddTabBar(node)
|
|
for n = 1, node.Windows.Size do
|
|
ImGui.TabBarAddTab(node.TabBar, ImGuiTabItemFlags.None, node.Windows.Data[n])
|
|
end
|
|
end
|
|
|
|
if payload_node ~= nil then
|
|
-- Transfer full payload node (with 1+ child windows or child nodes)
|
|
if payload_node:IsSplitNode() then
|
|
if node.Windows.Size > 0 then
|
|
-- We can dock a split payload into a node that already has windows _only_ if our payload is a node tree with a single visible node.
|
|
-- In this situation, we move the windows of the target node into the currently visible node of the payload.
|
|
-- This allows us to preserve some of the underlying dock tree settings nicely.
|
|
IM_ASSERT(payload_node.OnlyNodeWithWindows ~= nil) -- The docking should have been blocked by DockNodePreviewDockSetup() early on and never submitted.
|
|
local visible_node = payload_node.OnlyNodeWithWindows
|
|
if visible_node.TabBar then IM_ASSERT(visible_node.TabBar.Tabs.Size > 0) end
|
|
ImGui.DockNodeMoveWindows(node, visible_node)
|
|
ImGui.DockNodeMoveWindows(visible_node, node)
|
|
DockSettingsRenameNodeReferences(node.ID, visible_node.ID)
|
|
end
|
|
if node:IsCentralNode() then
|
|
-- Central node property needs to be moved to a leaf node, pick the last focused one.
|
|
-- FIXME-DOCK: If we had to transfer other flags here, what would the policy be?
|
|
local last_focused_node = ImGui.DockContextFindNodeByID(ctx, payload_node.LastFocusedNodeId)
|
|
IM_ASSERT(last_focused_node ~= nil)
|
|
local last_focused_root_node = ImGui.DockNodeGetRootNode(last_focused_node)
|
|
IM_ASSERT(last_focused_root_node == ImGui.DockNodeGetRootNode(payload_node))
|
|
last_focused_node:SetLocalFlags(bit32.bor(last_focused_node.LocalFlags, ImGuiDockNodeFlags.CentralNode))
|
|
node:SetLocalFlags(bit32.band(node.LocalFlags, bit32.bnot(ImGuiDockNodeFlags.CentralNode)))
|
|
last_focused_root_node.CentralNode = last_focused_node
|
|
end
|
|
|
|
IM_ASSERT(node.Windows.Size == 0)
|
|
ImGui.DockNodeMoveChildNodes(node, payload_node)
|
|
else
|
|
local payload_dock_id = payload_node.ID
|
|
ImGui.DockNodeMoveWindows(node, payload_node)
|
|
DockSettingsRenameNodeReferences(payload_dock_id, node.ID)
|
|
end
|
|
ImGui.DockContextRemoveNode(ctx, payload_node, true)
|
|
elseif payload_window then
|
|
-- Transfer single window
|
|
local payload_dock_id = payload_window.DockId
|
|
node.VisibleWindow = payload_window
|
|
ImGui.DockNodeAddWindow(node, payload_window, true)
|
|
if payload_dock_id ~= 0 then
|
|
DockSettingsRenameNodeReferences(payload_dock_id, node.ID)
|
|
end
|
|
end
|
|
else
|
|
-- When docking a floating single window node we want to reevaluate auto-hiding of the tab bar
|
|
node.WantHiddenTabBarUpdate = true
|
|
end
|
|
|
|
-- Update selection immediately
|
|
local tab_bar = node.TabBar
|
|
if tab_bar then tab_bar.NextSelectedTabId = next_selected_id end
|
|
ImGui.MarkIniSettingsDirty()
|
|
end
|
|
|
|
-- Problem:
|
|
-- Undocking a large (~full screen) window would leave it so large that the bottom right sizing corner would more
|
|
-- than likely be off the screen and the window would be hard to resize to fit on screen. This can be particularly problematic
|
|
-- with 'ConfigWindowsMoveFromTitleBarOnly=true' and/or with 'ConfigWindowsResizeFromEdges=false' as well (the later can be
|
|
-- due to missing ImGuiBackendFlags.HasMouseCursors backend flag).
|
|
-- Solution:
|
|
-- When undocking a window we currently force its maximum size to 90% of the host viewport.
|
|
-- Reevaluate this when we implement preserving docked/undocked size ("docking_wip/undocked_size" branch).
|
|
local function FixLargeWindowsWhenUndocking(size, ref_viewport)
|
|
if ref_viewport == nil then return size end
|
|
local max_size = ImTrunc(ref_viewport.WorkSize * 0.90)
|
|
return ImVec2(ImMin(size.x, max_size.x), ImMin(size.y, max_size.y))
|
|
end
|
|
|
|
function ImGui.DockContextProcessUndockWindow(ctx, window, clear_persistent_docking_ref)
|
|
if clear_persistent_docking_ref == nil then clear_persistent_docking_ref = true end -- C++ default argument
|
|
if window.DockNode then
|
|
ImGui.DockNodeRemoveWindow(window.DockNode, window, clear_persistent_docking_ref and 0 or window.DockId)
|
|
else
|
|
window.DockId = 0
|
|
end
|
|
window.Collapsed = false
|
|
window.DockIsActive = false
|
|
window.DockNodeIsVisible = false
|
|
window.DockTabIsVisible = false
|
|
local sz = FixLargeWindowsWhenUndocking(window.SizeFull, window.Viewport)
|
|
ImVec2_Copy(window.Size, sz)
|
|
ImVec2_Copy(window.SizeFull, sz)
|
|
ImGui.MarkIniSettingsDirty()
|
|
end
|
|
|
|
function ImGui.DockContextProcessUndockNode(ctx, node)
|
|
IM_ASSERT(node:IsLeafNode())
|
|
IM_ASSERT(node.Windows.Size >= 1)
|
|
|
|
if node:IsRootNode() or node:IsCentralNode() then
|
|
-- In the case of a root node or central node, the node will have to stay in place. Create a new node to receive the payload.
|
|
local new_node = ImGui.DockContextAddNode(ctx, 0)
|
|
new_node.Pos = ImVec2(node.Pos.x, node.Pos.y)
|
|
new_node.Size = ImVec2(node.Size.x, node.Size.y)
|
|
new_node.SizeRef = ImVec2(node.SizeRef.x, node.SizeRef.y)
|
|
ImGui.DockNodeMoveWindows(new_node, node)
|
|
DockSettingsRenameNodeReferences(node.ID, new_node.ID)
|
|
node = new_node
|
|
else
|
|
-- Otherwise extract our node and merge our sibling back into the parent node.
|
|
IM_ASSERT(node.ParentNode.ChildNodes[0] == node or node.ParentNode.ChildNodes[1] == node)
|
|
local index_in_parent = (node.ParentNode.ChildNodes[0] == node) and 0 or 1
|
|
node.ParentNode.ChildNodes[index_in_parent] = nil
|
|
ImGui.DockNodeTreeMerge(ctx, node.ParentNode, node.ParentNode.ChildNodes[bit32.bxor(index_in_parent, 1)])
|
|
node.ParentNode.AuthorityForViewport = ImGuiDataAuthority.Window -- The node that stays in place keeps the viewport, so our newly dragged out node will create a new viewport
|
|
node.ParentNode = nil
|
|
end
|
|
for _, window in node.Windows:iter() do
|
|
window.Flags = bit32.band(window.Flags, bit32.bnot(ImGuiWindowFlags.ChildWindow))
|
|
if window.ParentWindow then window.ParentWindow.DC.ChildWindows:find_erase(window) end
|
|
ImGui.UpdateWindowParentAndRootLinks(window, window.Flags, nil)
|
|
end
|
|
node.AuthorityForPos = ImGuiDataAuthority.DockNode
|
|
node.AuthorityForSize = ImGuiDataAuthority.DockNode
|
|
node.Size = FixLargeWindowsWhenUndocking(node.Size, node.Windows.Data[1].Viewport)
|
|
node.WantMouseMove = true
|
|
ImGui.MarkIniSettingsDirty()
|
|
end
|
|
|
|
-- This is mostly used for automation.
|
|
--- @return bool ok, ImVec2? out_pos
|
|
function ImGui.DockContextCalcDropPosForDocking(target, target_node, payload_window, payload_node, split_dir, split_outer)
|
|
if target ~= nil and target_node == nil then target_node = target.DockNode end
|
|
|
|
-- In DockNodePreviewDockSetup() for a root central node instead of showing both "inner" and "outer" drop rects
|
|
-- (which would be functionally identical) we only show the outer one. Reflect this here.
|
|
if target_node and target_node.ParentNode == nil and target_node:IsCentralNode() and split_dir ~= ImGuiDir.None then
|
|
split_outer = true
|
|
end
|
|
local split_data = ImGuiDockPreviewData()
|
|
ImGui.DockNodePreviewDockSetup(target, target_node, payload_window, payload_node, split_data, false, split_outer)
|
|
local r = split_data.DropRectsDraw[split_dir + 1]
|
|
if IsRectInverted(r) then return false, nil end
|
|
return true, r:GetCenter()
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: ImGuiDockNode
|
|
-------------------------------------------------------------------------------
|
|
-- - DockNodeGetTabOrder()
|
|
-- - DockNodeAddWindow()
|
|
-- - DockNodeRemoveWindow()
|
|
-- - DockNodeMoveChildNodes()
|
|
-- - DockNodeMoveWindows()
|
|
-- - DockNodeApplyPosSizeToWindows()
|
|
-- - DockNodeHideHostWindow()
|
|
-- - ImGuiDockNodeFindInfoResults
|
|
-- - DockNodeFindInfo()
|
|
-- - DockNodeFindWindowByID()
|
|
-- - DockNodeUpdateFlagsAndCollapse()
|
|
-- - DockNodeUpdateHasCentralNodeFlag()
|
|
-- - DockNodeUpdateVisibleFlag()
|
|
-- - DockNodeStartMouseMovingWindow()
|
|
-- - DockNodeUpdate()
|
|
-- - DockNodeUpdateWindowMenu()
|
|
-- - DockNodeBeginAmendTabBar()
|
|
-- - DockNodeEndAmendTabBar()
|
|
-- - DockNodeUpdateTabBar()
|
|
-- - DockNodeAddTabBar()
|
|
-- - DockNodeRemoveTabBar()
|
|
-- - DockNodeIsDropAllowedOne()
|
|
-- - DockNodeIsDropAllowed()
|
|
-- - DockNodeCalcTabBarLayout()
|
|
-- - DockNodeCalcSplitRects()
|
|
-- - DockNodeCalcDropRectsAndTestMousePos()
|
|
-- - DockNodePreviewDockSetup()
|
|
-- - DockNodePreviewDockRender()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.DockNodeGetTabOrder(window)
|
|
local tab_bar = window.DockNode.TabBar
|
|
if tab_bar == nil then return -1 end
|
|
local tab = ImGui.TabBarFindTabByID(tab_bar, window.TabId)
|
|
return tab and ImGui.TabBarGetTabOrder(tab_bar, tab) or -1
|
|
end
|
|
|
|
local function DockNodeHideWindowDuringHostWindowCreation(window)
|
|
window.Hidden = true
|
|
window.HiddenFramesCanSkipItems = window.Active and 1 or 2
|
|
end
|
|
|
|
function ImGui.DockNodeAddWindow(node, window, add_to_tab_bar)
|
|
if window.DockNode then
|
|
-- Can overwrite an existing window.DockNode (e.g. pointing to a disabled DockSpace node)
|
|
IM_ASSERT(window.DockNode.ID ~= node.ID)
|
|
ImGui.DockNodeRemoveWindow(window.DockNode, window, 0)
|
|
end
|
|
IM_ASSERT(window.DockNode == nil or window.DockNodeAsHost == nil)
|
|
|
|
-- If more than 2 windows appeared on the same frame leading to the creation of a new hosting window,
|
|
-- we'll hide windows until the host window is ready. Hide the 1st window after its been output (so it is not visible for one frame).
|
|
-- We will call DockNodeHideWindowDuringHostWindowCreation() on ourselves in Begin()
|
|
if node.HostWindow == nil and node.Windows.Size == 1 and node.Windows.Data[1].WasActive == false then
|
|
DockNodeHideWindowDuringHostWindowCreation(node.Windows.Data[1])
|
|
end
|
|
|
|
node.Windows:push_back(window)
|
|
node.WantHiddenTabBarUpdate = true
|
|
window.DockNode = node
|
|
window.DockId = node.ID
|
|
window.DockIsActive = (node.Windows.Size > 1)
|
|
window.DockTabWantClose = false
|
|
|
|
-- When reactivating a node with one or two loose window, the window pos/size/viewport are authoritative over the node storage.
|
|
-- In particular it is important we init the viewport from the first window so we don't create two viewports and drop one.
|
|
if node.HostWindow == nil and node:IsFloatingNode() then
|
|
if node.AuthorityForPos == ImGuiDataAuthority.Auto then node.AuthorityForPos = ImGuiDataAuthority.Window end
|
|
if node.AuthorityForSize == ImGuiDataAuthority.Auto then node.AuthorityForSize = ImGuiDataAuthority.Window end
|
|
if node.AuthorityForViewport == ImGuiDataAuthority.Auto then node.AuthorityForViewport = ImGuiDataAuthority.Window end
|
|
end
|
|
|
|
-- Add to tab bar if requested
|
|
if add_to_tab_bar then
|
|
if node.TabBar == nil then
|
|
ImGui.DockNodeAddTabBar(node)
|
|
node.TabBar.SelectedTabId = node.SelectedTabId
|
|
node.TabBar.NextSelectedTabId = node.SelectedTabId
|
|
|
|
-- Add existing windows
|
|
for n = 1, node.Windows.Size - 1 do
|
|
ImGui.TabBarAddTab(node.TabBar, ImGuiTabItemFlags.None, node.Windows.Data[n])
|
|
end
|
|
end
|
|
ImGui.TabBarAddTab(node.TabBar, ImGuiTabItemFlags.Unsorted, window)
|
|
end
|
|
|
|
ImGui.DockNodeUpdateVisibleFlag(node)
|
|
|
|
-- Update this without waiting for the next time we Begin() in the window, so our host window will have the proper title bar color on its first frame.
|
|
if node.HostWindow then
|
|
ImGui.UpdateWindowParentAndRootLinks(window, bit32.bor(window.Flags, ImGuiWindowFlags.ChildWindow), node.HostWindow)
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeRemoveWindow(node, window, save_dock_id)
|
|
IM_ASSERT(window.DockNode == node)
|
|
-- IM_ASSERT(window.RootWindowDockTree == node.HostWindow)
|
|
-- IM_ASSERT(window.LastFrameActive < g.FrameCount) -- We may call this from Begin()
|
|
IM_ASSERT(save_dock_id == 0 or save_dock_id == node.ID)
|
|
|
|
window.DockNode = nil
|
|
window.DockIsActive = false
|
|
window.DockTabWantClose = false
|
|
window.DockId = save_dock_id
|
|
window.Flags = bit32.band(window.Flags, bit32.bnot(ImGuiWindowFlags.ChildWindow))
|
|
if window.ParentWindow then window.ParentWindow.DC.ChildWindows:find_erase(window) end
|
|
ImGui.UpdateWindowParentAndRootLinks(window, window.Flags, nil) -- Update immediately
|
|
|
|
if node.HostWindow and node.HostWindow.ViewportOwned then
|
|
-- When undocking from a user interaction this will always run in NewFrame() and have not much effect.
|
|
-- But mid-frame, if we clear viewport we need to mark window as hidden as well.
|
|
window.Viewport = nil
|
|
window.ViewportId = 0
|
|
window.ViewportOwned = false
|
|
window.Hidden = true
|
|
end
|
|
|
|
-- Remove window
|
|
local idx = node.Windows:find_index(window)
|
|
IM_ASSERT(idx ~= nil)
|
|
node.Windows:erase(idx)
|
|
if node.VisibleWindow == window then node.VisibleWindow = nil end
|
|
|
|
-- Remove tab and possibly tab bar
|
|
node.WantHiddenTabBarUpdate = true
|
|
if node.TabBar then
|
|
ImGui.TabBarRemoveTab(node.TabBar, window.TabId)
|
|
local tab_count_threshold_for_tab_bar = node:IsCentralNode() and 1 or 2
|
|
if node.Windows.Size < tab_count_threshold_for_tab_bar then
|
|
ImGui.DockNodeRemoveTabBar(node)
|
|
end
|
|
end
|
|
|
|
if node.Windows.Size == 0 and not node:IsCentralNode() and not node:IsDockSpace() and window.DockId ~= node.ID then
|
|
-- Automatic dock node delete themselves if they are not holding at least one tab
|
|
ImGui.DockContextRemoveNode(ImGui.GetCurrentContext(), node, true)
|
|
return
|
|
end
|
|
|
|
if node.Windows.Size == 1 and not node:IsCentralNode() and node.HostWindow then
|
|
local remaining_window = node.Windows.Data[1]
|
|
-- Note: we used to transport viewport ownership here.
|
|
remaining_window.Collapsed = node.HostWindow.Collapsed
|
|
end
|
|
|
|
-- Update visibility immediately is required so the DockNodeUpdateRemoveInactiveChilds() processing can reflect changes up the tree
|
|
ImGui.DockNodeUpdateVisibleFlag(node)
|
|
end
|
|
|
|
function ImGui.DockNodeMoveChildNodes(dst_node, src_node)
|
|
IM_ASSERT(dst_node.Windows.Size == 0)
|
|
dst_node.ChildNodes[0] = src_node.ChildNodes[0]
|
|
dst_node.ChildNodes[1] = src_node.ChildNodes[1]
|
|
if dst_node.ChildNodes[0] then dst_node.ChildNodes[0].ParentNode = dst_node end
|
|
if dst_node.ChildNodes[1] then dst_node.ChildNodes[1].ParentNode = dst_node end
|
|
dst_node.SplitAxis = src_node.SplitAxis
|
|
dst_node.SizeRef = ImVec2(src_node.SizeRef.x, src_node.SizeRef.y)
|
|
src_node.ChildNodes[0] = nil
|
|
src_node.ChildNodes[1] = nil
|
|
end
|
|
|
|
function ImGui.DockNodeMoveWindows(dst_node, src_node)
|
|
-- Insert tabs in the same orders as currently ordered (node.Windows isn't ordered)
|
|
IM_ASSERT(src_node and dst_node and dst_node ~= src_node)
|
|
local src_tab_bar = src_node.TabBar
|
|
if src_tab_bar ~= nil then IM_ASSERT(src_node.Windows.Size <= src_node.TabBar.Tabs.Size) end
|
|
|
|
-- If the dst_node is empty we can just move the entire tab bar (to preserve selection, scrolling, etc.)
|
|
local move_tab_bar = (src_tab_bar ~= nil) and (dst_node.TabBar == nil)
|
|
if move_tab_bar then
|
|
dst_node.TabBar = src_node.TabBar
|
|
src_node.TabBar = nil
|
|
end
|
|
|
|
-- Tab order is not important here, it is preserved by sorting in DockNodeUpdateTabBar().
|
|
-- Snapshot: DockNodeAddWindow() won't touch src_node.Windows since window.DockNode is cleared first
|
|
local windows = {}
|
|
for i = 1, src_node.Windows.Size do windows[i] = src_node.Windows.Data[i] end
|
|
for _, window in ipairs(windows) do
|
|
window.DockNode = nil
|
|
window.DockIsActive = false
|
|
ImGui.DockNodeAddWindow(dst_node, window, not move_tab_bar)
|
|
end
|
|
src_node.Windows:clear()
|
|
|
|
if not move_tab_bar and src_node.TabBar then
|
|
if dst_node.TabBar then dst_node.TabBar.SelectedTabId = src_node.TabBar.SelectedTabId end
|
|
ImGui.DockNodeRemoveTabBar(src_node)
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeApplyPosSizeToWindows(node)
|
|
for _, window in node.Windows:iter() do
|
|
ImGui.SetWindowPos(window, node.Pos, ImGuiCond.Always) -- We don't assign directly to Pos because it can break the calculation of SizeContents on next frame
|
|
ImGui.SetWindowSize(window, node.Size, ImGuiCond.Always)
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeHideHostWindow(node)
|
|
if node.HostWindow then
|
|
if node.HostWindow.DockNodeAsHost == node then node.HostWindow.DockNodeAsHost = nil end
|
|
node.HostWindow = nil
|
|
end
|
|
if node.Windows.Size == 1 then
|
|
node.VisibleWindow = node.Windows.Data[1]
|
|
node.Windows.Data[1].DockIsActive = false
|
|
end
|
|
if node.TabBar then ImGui.DockNodeRemoveTabBar(node) end
|
|
end
|
|
|
|
-- Search function called once by root node in DockNodeUpdate()
|
|
local function DockNodeFindInfo(node, info)
|
|
if node.Windows.Size > 0 then
|
|
if info.FirstNodeWithWindows == nil then info.FirstNodeWithWindows = node end
|
|
info.CountNodesWithWindows = info.CountNodesWithWindows + 1
|
|
end
|
|
if node:IsCentralNode() then
|
|
IM_ASSERT(info.CentralNode == nil) -- Should be only one
|
|
IM_ASSERT(node:IsLeafNode(), "If you get this assert: please submit .ini file + repro of actions leading to this.")
|
|
info.CentralNode = node
|
|
end
|
|
if info.CountNodesWithWindows > 1 and info.CentralNode ~= nil then return end
|
|
if node.ChildNodes[0] then DockNodeFindInfo(node.ChildNodes[0], info) end
|
|
if node.ChildNodes[1] then DockNodeFindInfo(node.ChildNodes[1], info) end
|
|
end
|
|
|
|
function ImGui.DockNodeFindWindowByID(node, id)
|
|
IM_ASSERT(id ~= 0)
|
|
for _, window in node.Windows:iter() do
|
|
if window.ID == id then return window end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- - Remove inactive windows/nodes.
|
|
-- - Update visibility flag.
|
|
function ImGui.DockNodeUpdateFlagsAndCollapse(node)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(node.ParentNode == nil or node.ParentNode.ChildNodes[0] == node or node.ParentNode.ChildNodes[1] == node)
|
|
|
|
-- Inherit most flags
|
|
if node.ParentNode then
|
|
node.SharedFlags = bit32.band(node.ParentNode.SharedFlags, ImGuiDockNodeFlags.SharedFlagsInheritMask_)
|
|
end
|
|
|
|
-- Recurse into children
|
|
-- There is the possibility that one of our child becoming empty will delete itself and moving its sibling contents into 'node'.
|
|
-- If 'node.ChildNode[0]' delete itself, then 'node.ChildNode[1].Windows' will be moved into 'node'
|
|
-- If 'node.ChildNode[1]' delete itself, then 'node.ChildNode[0].Windows' will be moved into 'node' and the "remove inactive windows" loop will have run twice on those windows (harmless)
|
|
node.HasCentralNodeChild = false
|
|
if node.ChildNodes[0] then ImGui.DockNodeUpdateFlagsAndCollapse(node.ChildNodes[0]) end
|
|
if node.ChildNodes[1] then ImGui.DockNodeUpdateFlagsAndCollapse(node.ChildNodes[1]) end
|
|
|
|
-- Remove inactive windows, collapse nodes
|
|
-- Merge node flags overrides stored in windows
|
|
node.LocalFlagsInWindows = ImGuiDockNodeFlags.None
|
|
local window_n = 1
|
|
while window_n <= node.Windows.Size do
|
|
local window = node.Windows.Data[window_n]
|
|
IM_ASSERT(window.DockNode == node)
|
|
|
|
local node_was_active = (node.LastFrameActive + 1 == g.FrameCount)
|
|
local remove = false
|
|
remove = remove or (node_was_active and (window.WasActive == false)) -- Can't use 'window.LastFrameActive + 1 < g.FrameCount'. (see #9151)
|
|
remove = remove or (node_was_active and (node.WantCloseAll or node.WantCloseTabId == window.TabId) and window.HasCloseButton and bit32.band(window.Flags, ImGuiWindowFlags.UnsavedDocument) == 0) -- Submit all _expected_ closure from last frame
|
|
remove = remove or window.DockTabWantClose
|
|
if remove then
|
|
window.DockTabWantClose = false
|
|
if node.Windows.Size == 1 and not node:IsCentralNode() then
|
|
ImGui.DockNodeHideHostWindow(node)
|
|
node.State = ImGuiDockNodeState.HostWindowHiddenBecauseSingleWindow
|
|
ImGui.DockNodeRemoveWindow(node, window, node.ID) -- Will delete the node so it'll be invalid on return
|
|
return
|
|
end
|
|
ImGui.DockNodeRemoveWindow(node, window, node.ID)
|
|
else
|
|
-- FIXME-DOCKING: Missing policies for conflict resolution, hence the "Experimental" tag on this.
|
|
-- node.LocalFlagsInWindow = bit32.band(node.LocalFlagsInWindow, bit32.bnot(window.WindowClass.DockNodeFlagsOverrideClear))
|
|
node.LocalFlagsInWindows = bit32.bor(node.LocalFlagsInWindows, window.WindowClass.DockNodeFlagsOverrideSet)
|
|
window_n = window_n + 1
|
|
end
|
|
end
|
|
node:UpdateMergedFlags()
|
|
|
|
-- Auto-hide tab bar option
|
|
local node_flags = node.MergedFlags
|
|
if node.WantHiddenTabBarUpdate and node.Windows.Size == 1 and bit32.band(node_flags, ImGuiDockNodeFlags.AutoHideTabBar) ~= 0 and not node:IsHiddenTabBar() then
|
|
node.WantHiddenTabBarToggle = true
|
|
end
|
|
node.WantHiddenTabBarUpdate = false
|
|
|
|
-- Cancel toggling if we know our tab bar is enforced to be hidden at all times
|
|
if node.WantHiddenTabBarToggle and node.VisibleWindow and bit32.band(node.VisibleWindow.WindowClass.DockNodeFlagsOverrideSet, ImGuiDockNodeFlags.HiddenTabBar) ~= 0 then
|
|
node.WantHiddenTabBarToggle = false
|
|
end
|
|
|
|
-- Apply toggles at a single point of the frame (here!)
|
|
local prev_local_flags = node.LocalFlags
|
|
if node.Windows.Size > 1 then
|
|
node:SetLocalFlags(bit32.band(node.LocalFlags, bit32.bnot(ImGuiDockNodeFlags.HiddenTabBar)))
|
|
elseif node.WantHiddenTabBarToggle then
|
|
node:SetLocalFlags(bit32.bxor(node.LocalFlags, ImGuiDockNodeFlags.HiddenTabBar))
|
|
end
|
|
if bit32.band(bit32.bxor(node.LocalFlags, prev_local_flags), ImGuiDockNodeFlags.SavedFlagsMask_) ~= 0 then
|
|
ImGui.MarkIniSettingsDirty() -- Bit flaky to only do this here. Perhaps compare node flags every frame? #9380
|
|
end
|
|
node.WantHiddenTabBarToggle = false
|
|
|
|
ImGui.DockNodeUpdateVisibleFlag(node)
|
|
end
|
|
|
|
-- This is rarely called as DockNodeUpdateForRootNode() generally does it most frames.
|
|
function ImGui.DockNodeUpdateHasCentralNodeChild(node)
|
|
node.HasCentralNodeChild = false
|
|
if node.ChildNodes[0] then ImGui.DockNodeUpdateHasCentralNodeChild(node.ChildNodes[0]) end
|
|
if node.ChildNodes[1] then ImGui.DockNodeUpdateHasCentralNodeChild(node.ChildNodes[1]) end
|
|
if node:IsRootNode() then
|
|
local mark_node = node.CentralNode
|
|
while mark_node do
|
|
mark_node.HasCentralNodeChild = true
|
|
mark_node = mark_node.ParentNode
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeUpdateVisibleFlag(node)
|
|
-- Update visibility flag
|
|
local is_visible = (node.ParentNode == nil) and node:IsDockSpace() or node:IsCentralNode()
|
|
is_visible = is_visible or (node.Windows.Size > 0)
|
|
is_visible = is_visible or (node.ChildNodes[0] ~= nil and node.ChildNodes[0].IsVisible)
|
|
is_visible = is_visible or (node.ChildNodes[1] ~= nil and node.ChildNodes[1].IsVisible)
|
|
node.IsVisible = is_visible and true or false
|
|
end
|
|
|
|
function ImGui.DockNodeStartMouseMovingWindow(node, window)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(node.WantMouseMove == true)
|
|
ImGui.StartMouseMovingWindow(window)
|
|
ImVec2_Copy(g.ActiveIdClickOffset, g.IO.MouseClickedPos[0] - node.Pos)
|
|
g.MovingWindow = window -- If we are docked into a non moveable root window, StartMouseMovingWindow() won't set g.MovingWindow. Override that decision.
|
|
node.WantMouseMove = false
|
|
end
|
|
|
|
-- Update CentralNode, OnlyNodeWithWindows, LastFocusedNodeID. Copy window class.
|
|
function ImGui.DockNodeUpdateForRootNode(node)
|
|
ImGui.DockNodeUpdateFlagsAndCollapse(node)
|
|
|
|
-- - Setup central node pointers
|
|
-- - Find if there's only a single visible window in the hierarchy (in which case we need to display a regular title bar -> FIXME-DOCK: that last part is not done yet!)
|
|
-- Cannot merge this with DockNodeUpdateFlagsAndCollapse() because FirstNodeWithWindows is found after window removal and child collapsing
|
|
local info = { CentralNode = nil, FirstNodeWithWindows = nil, CountNodesWithWindows = 0 }
|
|
DockNodeFindInfo(node, info)
|
|
node.CentralNode = info.CentralNode
|
|
node.OnlyNodeWithWindows = (info.CountNodesWithWindows == 1) and info.FirstNodeWithWindows or nil
|
|
node.CountNodeWithWindows = info.CountNodesWithWindows
|
|
if node.LastFocusedNodeId == 0 and info.FirstNodeWithWindows ~= nil then
|
|
node.LastFocusedNodeId = info.FirstNodeWithWindows.ID
|
|
end
|
|
|
|
-- Copy the window class from of our first window so it can be used for proper dock filtering.
|
|
-- When node has mixed windows, prioritize the class with the most constraint (DockingAllowUnclassed = false) as the reference to copy.
|
|
-- FIXME-DOCK: We don't recurse properly, this code could be reworked to work from DockNodeUpdateScanRec.
|
|
local first_node_with_windows = info.FirstNodeWithWindows
|
|
if first_node_with_windows then
|
|
node.WindowClass = first_node_with_windows.Windows.Data[1].WindowClass
|
|
for n = 2, first_node_with_windows.Windows.Size do
|
|
if first_node_with_windows.Windows.Data[n].WindowClass.DockingAllowUnclassed == false then
|
|
node.WindowClass = first_node_with_windows.Windows.Data[n].WindowClass
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local mark_node = node.CentralNode
|
|
while mark_node do
|
|
mark_node.HasCentralNodeChild = true
|
|
mark_node = mark_node.ParentNode
|
|
end
|
|
end
|
|
|
|
local function DockNodeSetupHostWindow(node, host_window)
|
|
-- Remove ourselves from any previous different host window
|
|
-- This can happen if a user mistakenly does (see #4295 for details):
|
|
-- - N+0: DockBuilderAddNode(id, 0) -- missing ImGuiDockNodeFlags.DockSpace
|
|
-- - N+1: NewFrame() -- will create floating host window for that node
|
|
-- - N+1: DockSpace(id) -- requalify node as dockspace, moving host window
|
|
if node.HostWindow and node.HostWindow ~= host_window and node.HostWindow.DockNodeAsHost == node then
|
|
node.HostWindow.DockNodeAsHost = nil
|
|
end
|
|
host_window.DockNodeAsHost = node
|
|
node.HostWindow = host_window
|
|
end
|
|
|
|
function ImGui.DockNodeUpdate(node)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(node.LastFrameActive ~= g.FrameCount)
|
|
node.LastFrameAlive = g.FrameCount
|
|
node.IsBgDrawnThisFrame = false
|
|
|
|
node.CentralNode = nil
|
|
node.OnlyNodeWithWindows = nil
|
|
if node:IsRootNode() then ImGui.DockNodeUpdateForRootNode(node) end
|
|
|
|
-- Remove tab bar if not needed
|
|
if node.TabBar and node:IsNoTabBar() then ImGui.DockNodeRemoveTabBar(node) end
|
|
|
|
-- Early out for hidden root dock nodes (when all DockId references are in inactive windows, or there is only 1 floating window holding on the DockId)
|
|
local want_to_hide_host_window = false
|
|
if node:IsFloatingNode() then
|
|
if node.Windows.Size <= 1 and node:IsLeafNode() then
|
|
if not g.IO.ConfigDockingAlwaysTabBar and (node.Windows.Size == 0 or not node.Windows.Data[1].WindowClass.DockingAlwaysTabBar) then
|
|
want_to_hide_host_window = true
|
|
end
|
|
end
|
|
if node.CountNodeWithWindows == 0 then want_to_hide_host_window = true end
|
|
end
|
|
if want_to_hide_host_window then
|
|
if node.Windows.Size == 1 then
|
|
-- Floating window pos/size is authoritative
|
|
local single_window = node.Windows.Data[1]
|
|
node.Pos = ImVec2(single_window.Pos.x, single_window.Pos.y)
|
|
node.Size = ImVec2(single_window.SizeFull.x, single_window.SizeFull.y)
|
|
node.AuthorityForPos = ImGuiDataAuthority.Window
|
|
node.AuthorityForSize = ImGuiDataAuthority.Window
|
|
node.AuthorityForViewport = ImGuiDataAuthority.Window
|
|
|
|
-- Transfer focus immediately so when we revert to a regular window it is immediately selected
|
|
if node.HostWindow and g.NavWindow == node.HostWindow then
|
|
ImGui.FocusWindow(single_window)
|
|
end
|
|
if node.HostWindow then
|
|
single_window.Viewport = node.HostWindow.Viewport
|
|
single_window.ViewportId = node.HostWindow.ViewportId
|
|
end
|
|
node.RefViewportId = single_window.ViewportId or 0
|
|
end
|
|
|
|
ImGui.DockNodeHideHostWindow(node)
|
|
node.State = ImGuiDockNodeState.HostWindowHiddenBecauseSingleWindow
|
|
node.WantCloseAll = false
|
|
node.WantCloseTabId = 0
|
|
node.HasCloseButton = false
|
|
node.HasWindowMenuButton = false
|
|
node.LastFrameActive = g.FrameCount
|
|
|
|
if node.WantMouseMove and node.Windows.Size == 1 then
|
|
ImGui.DockNodeStartMouseMovingWindow(node, node.Windows.Data[1])
|
|
end
|
|
return
|
|
end
|
|
|
|
-- In some circumstance we will defer creating the host window (so everything will be kept hidden),
|
|
-- while the expected visible window is resizing itself.
|
|
-- This is important for first-time (no ini settings restored) single window when io.ConfigDockingAlwaysTabBar is enabled,
|
|
-- otherwise the node ends up using the minimum window size. Effectively those windows will take an extra frame to show up:
|
|
-- N+0: Begin(): window created (with no known size), node is created
|
|
-- N+1: DockNodeUpdate(): node skip creating host window / Begin(): window size applied, not visible
|
|
-- N+2: DockNodeUpdate(): node can create host window / Begin(): window becomes visible
|
|
-- We could remove this frame if we could reliably calculate the expected window size during node update, before the Begin() code.
|
|
-- It would require a generalization of CalcWindowExpectedSize(), probably extracting code away from Begin().
|
|
-- In reality it isn't very important as user quickly ends up with size data in .ini file.
|
|
if node.IsVisible and node.HostWindow == nil and node:IsFloatingNode() and node:IsLeafNode() then
|
|
IM_ASSERT(node.Windows.Size > 0)
|
|
local ref_window = nil
|
|
-- Note that we prune single-window-node settings on .ini loading, so this is generally 0 for them!
|
|
if node.SelectedTabId ~= 0 then ref_window = ImGui.DockNodeFindWindowByID(node, node.SelectedTabId) end
|
|
if ref_window == nil then ref_window = node.Windows.Data[1] end
|
|
if ref_window.AutoFitFramesX > 0 or ref_window.AutoFitFramesY > 0 then
|
|
node.State = ImGuiDockNodeState.HostWindowHiddenBecauseWindowsAreResizing
|
|
return
|
|
end
|
|
end
|
|
|
|
local node_flags = node.MergedFlags
|
|
|
|
-- Decide if the node will have a close button and a window menu button
|
|
node.HasWindowMenuButton = (node.Windows.Size > 0) and bit32.band(node_flags, ImGuiDockNodeFlags.NoWindowMenuButton) == 0
|
|
node.HasCloseButton = false
|
|
for _, window in node.Windows:iter() do
|
|
-- FIXME-DOCK: Setting DockIsActive here means that for single active window in a leaf node, DockIsActive will be cleared until the next Begin() call.
|
|
node.HasCloseButton = node.HasCloseButton or window.HasCloseButton
|
|
window.DockIsActive = (node.Windows.Size > 1)
|
|
end
|
|
if bit32.band(node_flags, ImGuiDockNodeFlags.NoCloseButton) ~= 0 or not g.Style.DockingNodeHasCloseButton then
|
|
node.HasCloseButton = false
|
|
end
|
|
|
|
-- Bind or create host window
|
|
local host_window = nil
|
|
local beginned_into_host_window = false
|
|
if node:IsDockSpace() then
|
|
-- [Explicit root dockspace node]
|
|
IM_ASSERT(node.HostWindow)
|
|
host_window = node.HostWindow
|
|
else
|
|
-- [Automatic root or child nodes]
|
|
if node:IsRootNode() and node.IsVisible then
|
|
local ref_window = (node.Windows.Size > 0) and node.Windows.Data[1] or nil
|
|
|
|
-- Sync Pos
|
|
if node.AuthorityForPos == ImGuiDataAuthority.Window and ref_window then
|
|
ImGui.SetNextWindowPos(ref_window.Pos)
|
|
elseif node.AuthorityForPos == ImGuiDataAuthority.DockNode then
|
|
ImGui.SetNextWindowPos(node.Pos)
|
|
end
|
|
|
|
-- Sync Size
|
|
if node.AuthorityForSize == ImGuiDataAuthority.Window and ref_window then
|
|
ImGui.SetNextWindowSize(ref_window.SizeFull)
|
|
elseif node.AuthorityForSize == ImGuiDataAuthority.DockNode then
|
|
ImGui.SetNextWindowSize(node.Size)
|
|
end
|
|
|
|
-- Sync Collapsed
|
|
if node.AuthorityForSize == ImGuiDataAuthority.Window and ref_window then
|
|
ImGui.SetNextWindowCollapsed(ref_window.Collapsed)
|
|
end
|
|
|
|
ImGui.SetNextWindowClass(node.WindowClass)
|
|
|
|
-- Begin into the host window
|
|
local window_label = DockNodeGetHostWindowTitle(node)
|
|
local window_flags = bit32.bor(ImGuiWindowFlags.NoScrollbar, ImGuiWindowFlags.NoScrollWithMouse, ImGuiWindowFlags.DockNodeHost)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoFocusOnAppearing)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoSavedSettings, ImGuiWindowFlags.NoNavFocus, ImGuiWindowFlags.NoCollapse)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoTitleBar)
|
|
|
|
ImGui.SetNextWindowBgAlpha(0.0) -- Don't set ImGuiWindowFlags.NoBackground because it disables borders
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, ImVec2(0, 0))
|
|
ImGui.Begin(window_label, nil, window_flags)
|
|
ImGui.PopStyleVar()
|
|
beginned_into_host_window = true
|
|
|
|
host_window = g.CurrentWindow
|
|
DockNodeSetupHostWindow(node, host_window)
|
|
ImVec2_Copy(host_window.DC.CursorPos, host_window.Pos)
|
|
node.Pos = ImVec2(host_window.Pos.x, host_window.Pos.y)
|
|
node.Size = ImVec2(host_window.Size.x, host_window.Size.y)
|
|
|
|
-- We set ImGuiWindowFlags.NoFocusOnAppearing because we don't want the host window to take full focus (e.g. steal NavWindow)
|
|
-- But we still it bring it to the front of display. There's no way to choose this precise behavior via window flags.
|
|
-- One simple case to ponder if: window A has a toggle to create windows B/C/D. Dock B/C/D together, clear the toggle and enable it again.
|
|
-- When reappearing B/C/D will request focus and be moved to the top of the display pile, but they are not linked to the dock host window
|
|
-- during the frame they appear. The dock host window would keep its old display order, and the sorting in EndFrame would move B/C/D back
|
|
-- after the dock host window, losing their top-most status.
|
|
if node.HostWindow.Appearing then ImGui.BringWindowToDisplayFront(node.HostWindow) end
|
|
|
|
node.AuthorityForPos = ImGuiDataAuthority.Auto
|
|
node.AuthorityForSize = ImGuiDataAuthority.Auto
|
|
node.AuthorityForViewport = ImGuiDataAuthority.Auto
|
|
elseif node.ParentNode then
|
|
host_window = node.ParentNode.HostWindow
|
|
node.HostWindow = host_window
|
|
node.AuthorityForPos = ImGuiDataAuthority.Auto
|
|
node.AuthorityForSize = ImGuiDataAuthority.Auto
|
|
node.AuthorityForViewport = ImGuiDataAuthority.Auto
|
|
end
|
|
if node.WantMouseMove and node.HostWindow then
|
|
ImGui.DockNodeStartMouseMovingWindow(node, node.HostWindow)
|
|
end
|
|
end
|
|
node.RefViewportId = 0 -- Clear when we have a host window
|
|
|
|
-- Update focused node (the one whose title bar is highlight) within a node tree
|
|
if node:IsSplitNode() then IM_ASSERT(node.TabBar == nil) end
|
|
if node:IsRootNode() then
|
|
local p_window = g.NavWindow and g.NavWindow.RootWindow or nil
|
|
while p_window ~= nil and p_window.DockNode ~= nil do
|
|
local p_node = ImGui.DockNodeGetRootNode(p_window.DockNode)
|
|
if p_node == node then
|
|
node.LastFocusedNodeId = p_window.DockNode.ID -- Note: not using root node ID!
|
|
break
|
|
end
|
|
p_window = p_node.HostWindow and p_node.HostWindow.RootWindow or nil
|
|
end
|
|
end
|
|
|
|
-- Register a hit-test hole in the window unless we are currently dragging a window that is compatible with our dockspace
|
|
local central_node = node.CentralNode
|
|
local central_node_hole = node:IsRootNode() and host_window ~= nil and bit32.band(node_flags, ImGuiDockNodeFlags.PassthruCentralNode) ~= 0 and central_node ~= nil and central_node:IsEmpty()
|
|
local central_node_hole_register_hit_test_hole = central_node_hole
|
|
if central_node_hole then
|
|
local payload = ImGui.GetDragDropPayload()
|
|
if payload and ImGuiPayload_IsDataType(payload, IMGUI_PAYLOAD_TYPE_WINDOW) and ImGui.DockNodeIsDropAllowed(host_window, payload.Data) then
|
|
central_node_hole_register_hit_test_hole = false
|
|
end
|
|
end
|
|
if central_node_hole_register_hit_test_hole then
|
|
-- We add a little padding to match the "resize from edges" behavior and allow grabbing the splitter easily.
|
|
-- (But we only add it if there's something else on the other side of the hole, otherwise for e.g. fullscreen
|
|
-- covering passthru node we'd have a gap on the edge not covered by the hole)
|
|
IM_ASSERT(node:IsDockSpace()) -- We cannot pass this flag without the DockSpace() api. Testing this because we also setup the hole in host_window.ParentNode
|
|
local root_node = ImGui.DockNodeGetRootNode(central_node)
|
|
local root_rect = ImRect(root_node.Pos, root_node.Pos + root_node.Size)
|
|
local hole_rect = ImRect(central_node.Pos, central_node.Pos + central_node.Size)
|
|
if hole_rect.Min.x > root_rect.Min.x then hole_rect.Min.x = hole_rect.Min.x + g.WindowsBorderHoverPadding end
|
|
if hole_rect.Max.x < root_rect.Max.x then hole_rect.Max.x = hole_rect.Max.x - g.WindowsBorderHoverPadding end
|
|
if hole_rect.Min.y > root_rect.Min.y then hole_rect.Min.y = hole_rect.Min.y + g.WindowsBorderHoverPadding end
|
|
if hole_rect.Max.y < root_rect.Max.y then hole_rect.Max.y = hole_rect.Max.y - g.WindowsBorderHoverPadding end
|
|
-- GetForegroundDrawList():AddRect(hole_rect.Min, hole_rect.Max, IM_COL32(255, 0, 0, 255))
|
|
if central_node_hole and not IsRectInverted(hole_rect) then
|
|
ImGui.SetWindowHitTestHole(host_window, hole_rect.Min, hole_rect.Max - hole_rect.Min)
|
|
if host_window.ParentWindow then
|
|
ImGui.SetWindowHitTestHole(host_window.ParentWindow, hole_rect.Min, hole_rect.Max - hole_rect.Min)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update position/size, process and draw resizing splitters
|
|
if node:IsRootNode() and host_window then
|
|
ImGui.DockNodeTreeUpdatePosSize(node, host_window.Pos, host_window.Size)
|
|
ImGui.PushStyleColor(ImGuiCol.Separator, g.Style.Colors[ImGuiCol.Border])
|
|
ImGui.PushStyleColor(ImGuiCol.SeparatorActive, g.Style.Colors[ImGuiCol.ResizeGripActive])
|
|
ImGui.PushStyleColor(ImGuiCol.SeparatorHovered, g.Style.Colors[ImGuiCol.ResizeGripHovered])
|
|
ImGui.DockNodeTreeUpdateSplitter(node)
|
|
ImGui.PopStyleColor(3)
|
|
end
|
|
|
|
-- Draw empty node background (currently can only be the Central Node)
|
|
if host_window and node:IsEmpty() and node.IsVisible then
|
|
host_window.DrawList:ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG)
|
|
node.LastBgColor = (bit32.band(node_flags, ImGuiDockNodeFlags.PassthruCentralNode) ~= 0) and 0 or ImGui.GetColorU32(ImGuiCol.DockingEmptyBg)
|
|
if node.LastBgColor ~= 0 then
|
|
host_window.DrawList:AddRectFilled(node.Pos, node.Pos + node.Size, node.LastBgColor)
|
|
end
|
|
node.IsBgDrawnThisFrame = true
|
|
end
|
|
|
|
-- Draw whole dockspace background if ImGuiDockNodeFlags.PassthruCentralNode if set.
|
|
-- We need to draw a background at the root level if requested by ImGuiDockNodeFlags.PassthruCentralNode, but we will only know the correct pos/size
|
|
-- _after_ processing the resizing splitters. So we are using the DrawList channel splitting facility to submit drawing primitives out of order!
|
|
local render_dockspace_bg = node:IsRootNode() and host_window ~= nil and bit32.band(node_flags, ImGuiDockNodeFlags.PassthruCentralNode) ~= 0
|
|
if render_dockspace_bg and node.IsVisible then
|
|
host_window.DrawList:ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG)
|
|
if central_node_hole then
|
|
ImGui.RenderRectFilledWithHole(host_window.DrawList, node:Rect(), central_node:Rect(), ImGui.GetColorU32(ImGuiCol.WindowBg), 0.0)
|
|
else
|
|
host_window.DrawList:AddRectFilled(node.Pos, node.Pos + node.Size, ImGui.GetColorU32(ImGuiCol.WindowBg), 0.0)
|
|
end
|
|
end
|
|
|
|
-- Draw and populate Tab Bar
|
|
if host_window then host_window.DrawList:ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG) end
|
|
if host_window and node.Windows.Size > 0 then
|
|
ImGui.DockNodeUpdateTabBar(node, host_window)
|
|
else
|
|
node.WantCloseAll = false
|
|
node.WantCloseTabId = 0
|
|
node.IsFocused = false
|
|
end
|
|
if node.TabBar and node.TabBar.SelectedTabId ~= 0 then
|
|
node.SelectedTabId = node.TabBar.SelectedTabId
|
|
elseif node.Windows.Size > 0 then
|
|
node.SelectedTabId = node.Windows.Data[1].TabId
|
|
end
|
|
|
|
-- Draw payload drop target
|
|
if host_window and node.IsVisible then
|
|
if node:IsRootNode() and (g.MovingWindow == nil or g.MovingWindow.RootWindowDockTree ~= host_window) then
|
|
ImGui.BeginDockableDragDropTarget(host_window)
|
|
end
|
|
end
|
|
|
|
-- We update this after DockNodeUpdateTabBar()
|
|
node.LastFrameActive = g.FrameCount
|
|
|
|
-- Recurse into children
|
|
-- FIXME-DOCK FIXME-OPT: Should not need to recurse into children
|
|
if host_window then
|
|
if node.ChildNodes[0] then ImGui.DockNodeUpdate(node.ChildNodes[0]) end
|
|
if node.ChildNodes[1] then ImGui.DockNodeUpdate(node.ChildNodes[1]) end
|
|
|
|
-- Render outer borders last (after the tab bar)
|
|
if node:IsRootNode() then ImGui.RenderWindowOuterBorders(host_window) end
|
|
end
|
|
|
|
-- End host window
|
|
if beginned_into_host_window then ImGui.End() end
|
|
end
|
|
|
|
local GWindowDockStyleColors = {
|
|
[0] = ImGuiCol.Text, ImGuiCol.TabHovered, ImGuiCol.Tab, ImGuiCol.TabSelected, ImGuiCol.TabSelectedOverline,
|
|
ImGuiCol.TabDimmed, ImGuiCol.TabDimmedSelected, ImGuiCol.TabDimmedSelectedOverline, ImGuiCol.UnsavedMarker,
|
|
}
|
|
ImGui.GWindowDockStyleColors = GWindowDockStyleColors
|
|
|
|
-- Compare TabItem nodes given the last known DockOrder (will persist in .ini file as hint), used to sort tabs when multiple tabs are added on the same frame.
|
|
local function TabItemComparerByDockOrder(ta, tb)
|
|
local a, b = ta.Window, tb.Window
|
|
local ao = (a.DockOrder == -1) and INT_MAX or a.DockOrder
|
|
local bo = (b.DockOrder == -1) and INT_MAX or b.DockOrder
|
|
if ao ~= bo then return ao < bo end
|
|
return a.BeginOrderWithinContext < b.BeginOrderWithinContext
|
|
end
|
|
|
|
-- Default handler for g.DockNodeWindowMenuHandler(): display the list of windows for a given dock-node.
|
|
-- This is exceptionally stored in a function pointer to also user applications to tweak this menu (undocumented)
|
|
-- Custom overrides may want to decorate, group, sort entries.
|
|
-- Please note those are internal structures: if you copy this expect occasional breakage.
|
|
-- (if you don't need to modify the "Tabs.Size == 1" behavior/path it is recommend you call this function in your handler)
|
|
function ImGui.DockNodeWindowMenuHandler_Default(ctx, node, tab_bar)
|
|
if tab_bar.Tabs.Size == 1 then
|
|
-- "Hide tab bar" option. Being one of our rare user-facing string we pull it from a table.
|
|
if ImGui.MenuItem(ImGui.LocalizeGetMsg(ImGuiLocKey.DockingHideTabBar), nil, node:IsHiddenTabBar()) then
|
|
node.WantHiddenTabBarToggle = true
|
|
end
|
|
else
|
|
-- Display a selectable list of windows in this docking node
|
|
for tab_n = 1, tab_bar.Tabs.Size do
|
|
local tab = tab_bar.Tabs.Data[tab_n]
|
|
if bit32.band(tab.Flags, ImGuiTabItemFlags.Button) == 0 then
|
|
if ImGui.Selectable(ImGui.TabBarGetTabName(tab_bar, tab), tab.ID == tab_bar.SelectedTabId) then
|
|
ImGui.TabBarQueueFocus(tab_bar, tab)
|
|
end
|
|
ImGui.SameLine()
|
|
ImGui.Text(" ")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeWindowMenuUpdate(node, tab_bar)
|
|
-- Try to position the menu so it is more likely to stays within the same viewport
|
|
local g = ImGui.GetCurrentContext()
|
|
if g.Style.WindowMenuButtonPosition == ImGuiDir.Left then
|
|
ImGui.SetNextWindowPos(ImVec2(node.Pos.x, node.Pos.y + ImGui.GetFrameHeight()), ImGuiCond.Always, ImVec2(0.0, 0.0))
|
|
else
|
|
ImGui.SetNextWindowPos(ImVec2(node.Pos.x + node.Size.x, node.Pos.y + ImGui.GetFrameHeight()), ImGuiCond.Always, ImVec2(1.0, 0.0))
|
|
end
|
|
if ImGui.BeginPopup("#WindowMenu") then
|
|
node.IsFocused = true
|
|
g.DockNodeWindowMenuHandler(g, node, tab_bar)
|
|
ImGui.EndPopup()
|
|
end
|
|
end
|
|
|
|
-- User helper to append/amend into a dock node tab bar. Most commonly used to add e.g. a "+" button.
|
|
function ImGui.DockNodeBeginAmendTabBar(node)
|
|
if node.TabBar == nil or node.HostWindow == nil then return false end
|
|
if bit32.band(node.MergedFlags, ImGuiDockNodeFlags.KeepAliveOnly) ~= 0 then return false end
|
|
if node.TabBar.ID == 0 then return false end
|
|
ImGui.Begin(node.HostWindow.Name)
|
|
ImGui.PushOverrideID(node.ID)
|
|
local ret = ImGui.BeginTabBarEx(node.TabBar, node.TabBar.BarRect, node.TabBar.Flags)
|
|
IM_ASSERT(ret)
|
|
return true
|
|
end
|
|
|
|
function ImGui.DockNodeEndAmendTabBar()
|
|
ImGui.EndTabBar()
|
|
ImGui.PopID()
|
|
ImGui.End()
|
|
end
|
|
|
|
local function IsDockNodeTitleBarHighlighted(node, root_node)
|
|
-- CTRL+Tab highlight (only highlighting leaf node, not whole hierarchy)
|
|
local g = ImGui.GetCurrentContext()
|
|
if g.NavWindowingTarget then
|
|
return g.NavWindowingTarget.DockNode == node
|
|
end
|
|
|
|
-- FIXME-DOCKING: May want alternative to treat central node void differently? e.g. if (g.NavWindow == host_window)
|
|
if g.NavWindow and root_node.LastFocusedNodeId == node.ID then
|
|
-- FIXME: This could all be backed in RootWindowForTitleBarHighlight? Probably need to reorganize for both dock nodes + other RootWindowForTitleBarHighlight users (not-node)
|
|
local parent_window = g.NavWindow.RootWindow
|
|
while bit32.band(parent_window.Flags, ImGuiWindowFlags.ChildMenu) ~= 0 do
|
|
parent_window = parent_window.ParentWindow.RootWindow
|
|
end
|
|
local parent_node = parent_window.DockNodeAsHost or parent_window.DockNode
|
|
while parent_node ~= nil do
|
|
parent_node = ImGui.DockNodeGetRootNode(parent_node)
|
|
if parent_node == root_node then return true end
|
|
parent_node = parent_node.HostWindow and parent_node.HostWindow.RootWindow.DockNode or nil
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Submit the tab bar corresponding to a dock node and various housekeeping details.
|
|
function ImGui.DockNodeUpdateTabBar(node, host_window)
|
|
local g = ImGui.GetCurrentContext()
|
|
local style = g.Style
|
|
|
|
local node_was_active = (node.LastFrameActive + 1 == g.FrameCount)
|
|
node.WantCloseAll = false
|
|
node.WantCloseTabId = 0
|
|
|
|
-- Decide if we should use a focused title bar color
|
|
local is_focused = false
|
|
local root_node = ImGui.DockNodeGetRootNode(node)
|
|
if IsDockNodeTitleBarHighlighted(node, root_node) then is_focused = true end
|
|
|
|
-- Hidden tab bar will show a triangle on the upper-left (in Begin)
|
|
if node:IsHiddenTabBar() or node:IsNoTabBar() then
|
|
node.VisibleWindow = (node.Windows.Size > 0) and node.Windows.Data[1] or nil
|
|
node.IsFocused = is_focused
|
|
if is_focused then node.LastFrameFocused = g.FrameCount end
|
|
if node.VisibleWindow then
|
|
-- Notify root of visible window (used to display title in OS task bar)
|
|
if is_focused or root_node.VisibleWindow == nil then root_node.VisibleWindow = node.VisibleWindow end
|
|
if node.TabBar then node.TabBar.VisibleTabId = node.VisibleWindow.TabId end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Move ourselves to the Menu layer (so we can be accessed by tapping Alt) + undo SkipItems flag in order to draw over the title bar even if the window is collapsed
|
|
local backup_skip_item = host_window.SkipItems
|
|
if not node:IsDockSpace() then
|
|
host_window.SkipItems = false
|
|
host_window.DC.NavLayerCurrent = ImGuiNavLayer.Menu
|
|
end
|
|
|
|
-- Use PushOverrideID() instead of PushID() to use the node id _without_ the host window ID.
|
|
-- This is to facilitate computing those ID from the outside, and will affect more or less only the ID of the collapse button, popup and tabs,
|
|
-- as docked windows themselves will override the stack with their own root ID.
|
|
ImGui.PushOverrideID(node.ID)
|
|
local tab_bar = node.TabBar
|
|
local tab_bar_is_recreated = (tab_bar == nil) -- Tab bar are automatically destroyed when a node gets hidden
|
|
if tab_bar == nil then
|
|
ImGui.DockNodeAddTabBar(node)
|
|
tab_bar = node.TabBar
|
|
end
|
|
|
|
local focus_tab_id = 0
|
|
node.IsFocused = is_focused
|
|
|
|
local node_flags = node.MergedFlags
|
|
local has_window_menu_button = bit32.band(node_flags, ImGuiDockNodeFlags.NoWindowMenuButton) == 0 and (style.WindowMenuButtonPosition ~= ImGuiDir.None)
|
|
|
|
-- In a dock node, the Collapse Button turns into the Window Menu button.
|
|
-- FIXME-DOCK FIXME-OPT: Could we recycle popups id across multiple dock nodes?
|
|
if has_window_menu_button and ImGui.IsPopupOpen("#WindowMenu") then
|
|
local next_selected_tab_id = tab_bar.NextSelectedTabId
|
|
ImGui.DockNodeWindowMenuUpdate(node, tab_bar)
|
|
if tab_bar.NextSelectedTabId ~= 0 and tab_bar.NextSelectedTabId ~= next_selected_tab_id then
|
|
focus_tab_id = tab_bar.NextSelectedTabId
|
|
end
|
|
is_focused = is_focused or node.IsFocused
|
|
end
|
|
|
|
-- Layout
|
|
local title_bar_rect, tab_bar_rect, window_menu_button_pos, close_button_pos = ImGui.DockNodeCalcTabBarLayout(node)
|
|
|
|
-- Submit new tabs, they will be added as Unsorted and sorted below based on relative DockOrder value.
|
|
local tabs_count_old = tab_bar.Tabs.Size
|
|
for window_n = 1, node.Windows.Size do
|
|
local window = node.Windows.Data[window_n]
|
|
if ImGui.TabBarFindTabByID(tab_bar, window.TabId) == nil then
|
|
ImGui.TabBarAddTab(tab_bar, ImGuiTabItemFlags.Unsorted, window)
|
|
end
|
|
end
|
|
|
|
-- Title bar
|
|
if is_focused then node.LastFrameFocused = g.FrameCount end
|
|
local title_bar_col = ImGui.GetColorU32(host_window.Collapsed and ImGuiCol.TitleBgCollapsed or (is_focused and ImGuiCol.TitleBgActive or ImGuiCol.TitleBg))
|
|
local rounding_flags = ImGui.CalcRoundingFlagsForRectInRect(title_bar_rect, host_window:Rect(), g.Style.DockingSeparatorSize)
|
|
host_window.DrawList:AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, host_window.WindowRounding, rounding_flags)
|
|
|
|
-- Docking/Collapse button
|
|
if has_window_menu_button then
|
|
if ImGui.CollapseButton(host_window:GetID("#COLLAPSE"), window_menu_button_pos, node) then -- == DockNodeGetWindowMenuButtonId(node)
|
|
ImGui.OpenPopup("#WindowMenu")
|
|
end
|
|
if ImGui.IsItemActive() then focus_tab_id = tab_bar.SelectedTabId end
|
|
if ImGui.IsItemHovered(bit32.bor(ImGuiHoveredFlags.ForTooltip, ImGuiHoveredFlags.DelayNormal)) and g.HoveredIdTimer > 0.5 then
|
|
ImGui.SetTooltip("%s", ImGui.LocalizeGetMsg(ImGuiLocKey.DockingDragToUndockOrMoveNode))
|
|
end
|
|
end
|
|
|
|
-- If multiple tabs are appearing on the same frame, sort them based on their persistent DockOrder value
|
|
local tabs_unsorted_start = tab_bar.Tabs.Size -- 0-based
|
|
local tab_n = tab_bar.Tabs.Size - 1
|
|
while tab_n >= 0 and bit32.band(tab_bar.Tabs.Data[tab_n + 1].Flags, ImGuiTabItemFlags.Unsorted) ~= 0 do
|
|
-- FIXME-DOCK: Consider only clearing the flag after the tab has been alive for a few consecutive frames, allowing late comers to not break sorting?
|
|
local t = tab_bar.Tabs.Data[tab_n + 1]
|
|
t.Flags = bit32.band(t.Flags, bit32.bnot(ImGuiTabItemFlags.Unsorted))
|
|
tabs_unsorted_start = tab_n
|
|
tab_n = tab_n - 1
|
|
end
|
|
if tab_bar.Tabs.Size > tabs_unsorted_start + 1 then
|
|
local slice = {}
|
|
for i = tabs_unsorted_start + 1, tab_bar.Tabs.Size do slice[#slice + 1] = tab_bar.Tabs.Data[i] end
|
|
table.sort(slice, TabItemComparerByDockOrder)
|
|
for i, t in ipairs(slice) do tab_bar.Tabs.Data[tabs_unsorted_start + i] = t end
|
|
end
|
|
|
|
-- Apply NavWindow focus back to the tab bar
|
|
if g.NavWindow and g.NavWindow.RootWindow.DockNode == node then
|
|
tab_bar.SelectedTabId = g.NavWindow.RootWindow.TabId
|
|
end
|
|
|
|
-- Selected newly added tabs, or persistent tab ID if the tab bar was just recreated
|
|
if tab_bar_is_recreated and ImGui.TabBarFindTabByID(tab_bar, node.SelectedTabId) ~= nil then
|
|
tab_bar.SelectedTabId = node.SelectedTabId
|
|
tab_bar.NextSelectedTabId = node.SelectedTabId
|
|
elseif tab_bar.Tabs.Size > tabs_count_old then
|
|
tab_bar.SelectedTabId = tab_bar.Tabs.Data[tab_bar.Tabs.Size].Window.TabId
|
|
tab_bar.NextSelectedTabId = tab_bar.SelectedTabId
|
|
end
|
|
|
|
-- Begin tab bar
|
|
local tab_bar_flags = bit32.bor(ImGuiTabBarFlags.Reorderable, ImGuiTabBarFlags.AutoSelectNewTabs) -- | ImGuiTabBarFlags.NoTabListScrollingButtons)
|
|
tab_bar_flags = bit32.bor(tab_bar_flags, ImGuiTabBarFlags.SaveSettings, ImGuiTabBarFlags.DockNode)
|
|
tab_bar_flags = bit32.bor(tab_bar_flags, ImGuiTabBarFlags.FittingPolicyMixed) -- Enforce default policy. Since 1.92.2 this is now reasonable. May expose later if needed. (#8800, #3421)
|
|
tab_bar_flags = bit32.bor(tab_bar_flags, ImGuiTabBarFlags.DrawSelectedOverline)
|
|
if not host_window.Collapsed and is_focused then
|
|
tab_bar_flags = bit32.bor(tab_bar_flags, ImGuiTabBarFlags.IsFocused)
|
|
end
|
|
tab_bar.ID = node.ID -- GetID("#TabBar")
|
|
tab_bar.SeparatorMinX = node.Pos.x + host_window.WindowBorderSize -- Separator cover the whole node width
|
|
tab_bar.SeparatorMaxX = node.Pos.x + node.Size.x - host_window.WindowBorderSize
|
|
ImGui.BeginTabBarEx(tab_bar, tab_bar_rect, tab_bar_flags)
|
|
-- host_window.DrawList:AddRect(tab_bar_rect.Min, tab_bar_rect.Max, IM_COL32(255,0,255,255))
|
|
|
|
-- Backup style colors
|
|
local backup_style_cols = {}
|
|
for color_n = 0, ImGuiWindowDockStyleCol.COUNT - 1 do
|
|
local c = g.Style.Colors[GWindowDockStyleColors[color_n]]
|
|
backup_style_cols[color_n] = ImVec4(c.x, c.y, c.z, c.w)
|
|
end
|
|
|
|
-- Submit actual tabs
|
|
node.VisibleWindow = nil
|
|
for window_n = 1, node.Windows.Size do
|
|
local window = node.Windows.Data[window_n]
|
|
-- FIXME: Not sure if that's still taken/useful, as windows are normally removed in DockNodeUpdateFlagsAndCollapse().
|
|
if not (window.LastFrameActive + 1 < g.FrameCount and node_was_active) then
|
|
local tab_item_flags = 0
|
|
tab_item_flags = bit32.bor(tab_item_flags, window.WindowClass.TabItemFlagsOverrideSet or 0)
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.UnsavedDocument) ~= 0 then
|
|
tab_item_flags = bit32.bor(tab_item_flags, ImGuiTabItemFlags.UnsavedDocument)
|
|
end
|
|
if bit32.band(tab_bar.Flags, ImGuiTabBarFlags.NoCloseWithMiddleMouseButton) ~= 0 then
|
|
tab_item_flags = bit32.bor(tab_item_flags, ImGuiTabItemFlags.NoCloseWithMiddleMouseButton)
|
|
end
|
|
|
|
-- Apply stored style overrides for the window
|
|
for color_n = 0, ImGuiWindowDockStyleCol.COUNT - 1 do
|
|
ImVec4_Copy(g.Style.Colors[GWindowDockStyleColors[color_n]], ImGui.ColorConvertU32ToFloat4(window.DockStyle.Colors[color_n]))
|
|
end
|
|
|
|
-- Note that TabItemEx() calls TabBarCalcTabID() so our tab item ID will ignore the current ID stack (rightly so)
|
|
local _, tab_open = ImGui.TabItemEx(tab_bar, window.Name, window.HasCloseButton and true or nil, tab_item_flags, window)
|
|
if tab_open == false then node.WantCloseTabId = window.TabId end
|
|
if tab_bar.VisibleTabId == window.TabId then node.VisibleWindow = window end
|
|
|
|
-- Store last item data so it can be queried with IsItemXXX functions after the user Begin() call
|
|
window.DC.DockTabItemStatusFlags = g.LastItemData.StatusFlags
|
|
window.DC.DockTabItemRect = ImRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max)
|
|
|
|
-- Update navigation ID on menu layer
|
|
if g.NavWindow and g.NavWindow.RootWindow == window and bit32.band(window.DC.NavLayersActiveMask, bit32.lshift(1, ImGuiNavLayer.Menu)) == 0 then
|
|
host_window.NavLastIds[1] = window.TabId
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Restore style colors
|
|
for color_n = 0, ImGuiWindowDockStyleCol.COUNT - 1 do
|
|
ImVec4_Copy(g.Style.Colors[GWindowDockStyleColors[color_n]], backup_style_cols[color_n])
|
|
end
|
|
|
|
-- Notify root of visible window (used to display title in OS task bar)
|
|
if node.VisibleWindow then
|
|
if is_focused or root_node.VisibleWindow == nil then root_node.VisibleWindow = node.VisibleWindow end
|
|
end
|
|
|
|
-- Close button (after VisibleWindow was updated)
|
|
-- Note that VisibleWindow may have been overrided by CTRL+Tabbing, so VisibleWindow.TabId may be ~= from tab_bar.SelectedTabId
|
|
local close_button_is_enabled = node.HasCloseButton and node.VisibleWindow and node.VisibleWindow.HasCloseButton
|
|
local close_button_is_visible = node.HasCloseButton
|
|
-- local close_button_is_visible = close_button_is_enabled -- Most people would expect this behavior of not even showing the button (leaving a hole since we can't claim that space as other windows in the tba bar have one)
|
|
if close_button_is_visible then
|
|
if not close_button_is_enabled then
|
|
ImGui.PushItemFlag(ImGuiItemFlags.Disabled, true)
|
|
local tc = style.Colors[ImGuiCol.Text]
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImVec4(tc.x, tc.y, tc.z, tc.w * 0.4))
|
|
end
|
|
if ImGui.CloseButton(host_window:GetID("#CLOSE"), close_button_pos) then
|
|
node.WantCloseAll = true
|
|
for n = 1, tab_bar.Tabs.Size do ImGui.TabBarCloseTab(tab_bar, tab_bar.Tabs.Data[n]) end
|
|
end
|
|
-- if IsItemActive() then
|
|
-- focus_tab_id = tab_bar.SelectedTabId
|
|
if not close_button_is_enabled then
|
|
ImGui.PopStyleColor()
|
|
ImGui.PopItemFlag()
|
|
end
|
|
end
|
|
|
|
-- When clicking on the title bar outside of tabs, we still focus the selected tab for that node
|
|
-- FIXME: TabItems submitted earlier use AllowItemOverlap so we manually perform a more specific test for now (hovered || held) in order to not cover them.
|
|
local title_bar_id = host_window:GetID("#TITLEBAR")
|
|
if g.HoveredId == 0 or g.HoveredId == title_bar_id or g.ActiveId == title_bar_id then
|
|
-- AllowOverlap mode required for appending into dock node tab bar,
|
|
-- otherwise dragging window will steal HoveredId and amended tabs cannot get them.
|
|
ImGui.KeepAliveID(title_bar_id)
|
|
local _, _, held = ImGui.ButtonBehavior(title_bar_rect, title_bar_id, ImGuiButtonFlags.AllowOverlap)
|
|
if g.HoveredId == title_bar_id then g.LastItemData.ID = title_bar_id end
|
|
if held then
|
|
if ImGui.IsMouseClicked(0) then focus_tab_id = tab_bar.SelectedTabId end
|
|
|
|
-- Forward moving request to selected window
|
|
local tab = ImGui.TabBarFindTabByID(tab_bar, tab_bar.SelectedTabId)
|
|
if tab then
|
|
ImGui.StartMouseMovingWindowOrNode(tab.Window or node.HostWindow, node, false) -- Undock from tab bar empty space
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Forward focus from host node to selected window
|
|
-- if is_focused and g.NavWindow == host_window and not g.NavWindowingTarget then
|
|
-- focus_tab_id = tab_bar.SelectedTabId
|
|
|
|
-- When clicked on a tab we requested focus to the docked child
|
|
-- This overrides the value set by "forward focus from host node to selected window".
|
|
if tab_bar.NextSelectedTabId ~= 0 then focus_tab_id = tab_bar.NextSelectedTabId end
|
|
|
|
-- Apply navigation focus
|
|
if focus_tab_id ~= 0 then
|
|
local tab = ImGui.TabBarFindTabByID(tab_bar, focus_tab_id)
|
|
if tab and tab.Window then
|
|
ImGui.FocusWindow(tab.Window)
|
|
if g.NavId == 0 then ImGui.NavInitWindow(tab.Window, false) end -- only init if FocusWindow() didn't restore anything.
|
|
end
|
|
end
|
|
|
|
ImGui.EndTabBar()
|
|
ImGui.PopID()
|
|
|
|
-- Restore SkipItems flag
|
|
if not node:IsDockSpace() then
|
|
host_window.DC.NavLayerCurrent = ImGuiNavLayer.Main
|
|
host_window.SkipItems = backup_skip_item
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeAddTabBar(node)
|
|
IM_ASSERT(node.TabBar == nil)
|
|
node.TabBar = ImGuiTabBar()
|
|
end
|
|
|
|
function ImGui.DockNodeRemoveTabBar(node)
|
|
if node.TabBar == nil then return end
|
|
node.TabBar = nil
|
|
end
|
|
|
|
local function DockNodeIsDropAllowedOne(payload, host_window)
|
|
if host_window.DockNodeAsHost and host_window.DockNodeAsHost:IsDockSpace() and payload.BeginOrderWithinContext < host_window.BeginOrderWithinContext then
|
|
return false
|
|
end
|
|
|
|
local host_class = host_window.DockNodeAsHost and host_window.DockNodeAsHost.WindowClass or host_window.WindowClass
|
|
local payload_class = payload.WindowClass
|
|
if host_class.ClassId ~= payload_class.ClassId then
|
|
local pass = false
|
|
if host_class.ClassId ~= 0 and host_class.DockingAllowUnclassed and payload_class.ClassId == 0 then pass = true end
|
|
if payload_class.ClassId ~= 0 and payload_class.DockingAllowUnclassed and host_class.ClassId == 0 then pass = true end
|
|
if not pass then return false end
|
|
end
|
|
|
|
-- Prevent docking any window created above a popup
|
|
-- Technically we should support it (e.g. in the case of a long-lived modal window that had fancy docking features),
|
|
-- by e.g. adding a 'if not ImGui.IsWindowWithinBeginStackOf(host_window, popup_window)' test.
|
|
-- But it would requires more work on our end because the dock host windows is technically created in NewFrame()
|
|
-- and our .ParentXXX and .RootXXX pointers inside windows are currently mislading or lacking.
|
|
local g = ImGui.GetCurrentContext()
|
|
for i = g.OpenPopupStack.Size, 1, -1 do
|
|
local popup_window = g.OpenPopupStack.Data[i].Window
|
|
if popup_window and ImGui.IsWindowWithinBeginStackOf(payload, popup_window) then -- Payload is created from within a popup begin stack.
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function ImGui.DockNodeIsDropAllowed(host_window, root_payload)
|
|
if root_payload.DockNodeAsHost and root_payload.DockNodeAsHost:IsSplitNode() then return true end -- FIXME-DOCK: Missing filtering
|
|
local payload_count = root_payload.DockNodeAsHost and root_payload.DockNodeAsHost.Windows.Size or 1
|
|
for payload_n = 1, payload_count do
|
|
local payload = root_payload.DockNodeAsHost and root_payload.DockNodeAsHost.Windows.Data[payload_n] or root_payload
|
|
if DockNodeIsDropAllowedOne(payload, host_window) then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- window menu button == collapse button when not in a dock node.
|
|
-- FIXME: This is similar to RenderWindowTitleBarContents(), may want to share code.
|
|
--- @return ImRect title_rect, ImRect tab_bar_rect, ImVec2 window_menu_button_pos, ImVec2? close_button_pos
|
|
function ImGui.DockNodeCalcTabBarLayout(node)
|
|
local g = ImGui.GetCurrentContext()
|
|
local style = g.Style
|
|
|
|
local r = ImRect(node.Pos.x, node.Pos.y, node.Pos.x + node.Size.x, node.Pos.y + g.FontSize + g.Style.FramePadding.y * 2.0)
|
|
local out_title_rect = ImRect(r.Min, r.Max)
|
|
|
|
r.Min.x = r.Min.x + style.WindowBorderSize
|
|
r.Max.x = r.Max.x - style.WindowBorderSize
|
|
|
|
local button_sz = g.FontSize
|
|
r.Min.x = r.Min.x + style.FramePadding.x
|
|
r.Max.x = r.Max.x - style.FramePadding.x
|
|
local window_menu_button_pos = ImVec2(r.Min.x, r.Min.y + style.FramePadding.y)
|
|
local out_close_button_pos = nil
|
|
if node.HasCloseButton then
|
|
out_close_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y)
|
|
r.Max.x = r.Max.x - (button_sz + style.ItemInnerSpacing.x)
|
|
end
|
|
if node.HasWindowMenuButton and style.WindowMenuButtonPosition == ImGuiDir.Left then
|
|
r.Min.x = r.Min.x + button_sz + style.ItemInnerSpacing.x
|
|
elseif node.HasWindowMenuButton and style.WindowMenuButtonPosition == ImGuiDir.Right then
|
|
window_menu_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y)
|
|
r.Max.x = r.Max.x - (button_sz + style.ItemInnerSpacing.x)
|
|
end
|
|
return out_title_rect, r, window_menu_button_pos, out_close_button_pos
|
|
end
|
|
|
|
--- Modifies pos_old/size_old/pos_new/size_new in place (ImVec2 tables)
|
|
function ImGui.DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, dir, size_new_desired)
|
|
local g = ImGui.GetCurrentContext()
|
|
local dock_spacing = g.Style.ItemInnerSpacing.x
|
|
local axis = (dir == ImGuiDir.Left or dir == ImGuiDir.Right) and ImGuiAxis.X or ImGuiAxis.Y
|
|
local a, o = axis, 3 - axis -- the port's ImGuiAxis is 1-based (X=1,Y=2): it is directly the ImVec2 index
|
|
pos_new[o] = pos_old[o]
|
|
size_new[o] = size_old[o]
|
|
|
|
-- Distribute size on given axis (with a desired size or equally)
|
|
local w_avail = size_old[a] - dock_spacing
|
|
if size_new_desired[a] > 0.0 and size_new_desired[a] <= w_avail * 0.5 then
|
|
size_new[a] = size_new_desired[a]
|
|
size_old[a] = IM_TRUNC(w_avail - size_new[a])
|
|
else
|
|
size_new[a] = IM_TRUNC(w_avail * 0.5)
|
|
size_old[a] = IM_TRUNC(w_avail - size_new[a])
|
|
end
|
|
|
|
-- Position each node
|
|
if dir == ImGuiDir.Right or dir == ImGuiDir.Down then
|
|
pos_new[a] = pos_old[a] + size_old[a] + dock_spacing
|
|
elseif dir == ImGuiDir.Left or dir == ImGuiDir.Up then
|
|
pos_new[a] = pos_old[a]
|
|
pos_old[a] = pos_new[a] + size_new[a] + dock_spacing
|
|
end
|
|
end
|
|
|
|
local function ImGetDirQuadrantFromDeltaLocal(dx, dy)
|
|
if math.abs(dx) > math.abs(dy) then
|
|
return (dx > 0.0) and ImGuiDir.Right or ImGuiDir.Left
|
|
end
|
|
return (dy > 0.0) and ImGuiDir.Down or ImGuiDir.Up
|
|
end
|
|
|
|
-- Retrieve the drop rectangles for a given direction or for the center + perform hit testing.
|
|
--- @return bool hit, ImRect out_r
|
|
function ImGui.DockNodeCalcDropRectsAndTestMousePos(parent, dir, outer_docking, test_mouse_pos)
|
|
local g = ImGui.GetCurrentContext()
|
|
|
|
local parent_smaller_axis = ImMin(parent:GetWidth(), parent:GetHeight())
|
|
local hs_for_central_nodes = ImMin(g.FontSize * 1.5, ImMax(g.FontSize * 0.5, parent_smaller_axis / 8.0))
|
|
local hs_w, hs_h, off -- Half-size, longer axis / Half-size, smaller axis / Distance from edge or center
|
|
if outer_docking then
|
|
-- hs_w = ImTrunc(ImClamp(parent_smaller_axis - hs_for_central_nodes * 4.0, g.FontSize * 0.5, g.FontSize * 8.0))
|
|
-- hs_h = ImTrunc(hs_w * 0.15)
|
|
-- off = ImVec2(ImTrunc(parent:GetWidth() * 0.5 - GetFrameHeightWithSpacing() * 1.4 - hs_h), ImTrunc(parent:GetHeight() * 0.5 - GetFrameHeightWithSpacing() * 1.4 - hs_h))
|
|
hs_w = ImTrunc(hs_for_central_nodes * 1.50)
|
|
hs_h = ImTrunc(hs_for_central_nodes * 0.80)
|
|
off = ImVec2(ImTrunc(parent:GetWidth() * 0.5 - hs_h), ImTrunc(parent:GetHeight() * 0.5 - hs_h))
|
|
else
|
|
hs_w = ImTrunc(hs_for_central_nodes)
|
|
hs_h = ImTrunc(hs_for_central_nodes * 0.90)
|
|
off = ImVec2(ImTrunc(hs_w * 2.40), ImTrunc(hs_w * 2.40))
|
|
end
|
|
|
|
local pc = parent:GetCenter()
|
|
local c = ImVec2(ImTrunc(pc.x), ImTrunc(pc.y))
|
|
local out_r
|
|
if dir == ImGuiDir.None then out_r = ImRect(c.x - hs_w, c.y - hs_w, c.x + hs_w, c.y + hs_w)
|
|
elseif dir == ImGuiDir.Up then out_r = ImRect(c.x - hs_w, c.y - off.y - hs_h, c.x + hs_w, c.y - off.y + hs_h)
|
|
elseif dir == ImGuiDir.Down then out_r = ImRect(c.x - hs_w, c.y + off.y - hs_h, c.x + hs_w, c.y + off.y + hs_h)
|
|
elseif dir == ImGuiDir.Left then out_r = ImRect(c.x - off.x - hs_h, c.y - hs_w, c.x - off.x + hs_h, c.y + hs_w)
|
|
elseif dir == ImGuiDir.Right then out_r = ImRect(c.x + off.x - hs_h, c.y - hs_w, c.x + off.x + hs_h, c.y + hs_w) end
|
|
|
|
if test_mouse_pos == nil then return false, out_r end
|
|
|
|
local hit_r = ImRect(out_r.Min, out_r.Max)
|
|
if not outer_docking then
|
|
-- Custom hit testing for the 5-way selection, designed to reduce flickering when moving diagonally between sides
|
|
hit_r:Expand(ImTrunc(hs_w * 0.30))
|
|
local mdx, mdy = test_mouse_pos.x - c.x, test_mouse_pos.y - c.y
|
|
local mouse_delta_len2 = mdx * mdx + mdy * mdy
|
|
local r_threshold_center = hs_w * 1.4
|
|
local r_threshold_sides = hs_w * (1.4 + 1.2)
|
|
if mouse_delta_len2 < r_threshold_center * r_threshold_center then
|
|
return (dir == ImGuiDir.None), out_r
|
|
end
|
|
if mouse_delta_len2 < r_threshold_sides * r_threshold_sides then
|
|
return (dir == ImGetDirQuadrantFromDeltaLocal(mdx, mdy)), out_r
|
|
end
|
|
end
|
|
return hit_r:Contains(test_mouse_pos), out_r
|
|
end
|
|
|
|
-- host_node may be nil if the window doesn't have a DockNode already.
|
|
-- FIXME-DOCK: This is misnamed since it's also doing the filtering.
|
|
function ImGui.DockNodePreviewDockSetup(host_window, host_node, payload_window, payload_node, data, is_explicit_target, is_outer_docking)
|
|
local g = ImGui.GetCurrentContext()
|
|
|
|
-- There is an edge case when docking into a dockspace which only has inactive nodes.
|
|
-- In this case DockNodeTreeFindNodeByPos() will have selected a leaf node which is inactive.
|
|
-- Because the inactive leaf node doesn't have proper pos/size yet, we'll use the root node as reference.
|
|
if payload_node == nil then payload_node = payload_window.DockNodeAsHost end
|
|
local ref_node_for_rect = (host_node and not host_node.IsVisible) and ImGui.DockNodeGetRootNode(host_node) or host_node
|
|
if ref_node_for_rect then IM_ASSERT(ref_node_for_rect.IsVisible == true) end
|
|
|
|
-- Filter, figure out where we are allowed to dock
|
|
local src_node_flags = payload_node and payload_node.MergedFlags or payload_window.WindowClass.DockNodeFlagsOverrideSet
|
|
local dst_node_flags = host_node and host_node.MergedFlags or host_window.WindowClass.DockNodeFlagsOverrideSet
|
|
data.IsCenterAvailable = true
|
|
if is_outer_docking then
|
|
data.IsCenterAvailable = false
|
|
elseif g.IO.ConfigDockingNoDockingOver then
|
|
data.IsCenterAvailable = false
|
|
elseif bit32.band(dst_node_flags, ImGuiDockNodeFlags.NoDockingOverMe) ~= 0 then
|
|
data.IsCenterAvailable = false
|
|
elseif host_node and bit32.band(dst_node_flags, ImGuiDockNodeFlags.NoDockingOverCentralNode) ~= 0 and host_node:IsCentralNode() then
|
|
data.IsCenterAvailable = false
|
|
elseif (not host_node or not host_node:IsEmpty()) and payload_node and payload_node:IsSplitNode() and (payload_node.OnlyNodeWithWindows == nil) then -- Is _visibly_ split?
|
|
data.IsCenterAvailable = false
|
|
elseif bit32.band(src_node_flags, ImGuiDockNodeFlags.NoDockingOverOther) ~= 0 and (not host_node or not host_node:IsEmpty()) then
|
|
data.IsCenterAvailable = false
|
|
elseif bit32.band(src_node_flags, ImGuiDockNodeFlags.NoDockingOverEmpty) ~= 0 and host_node and host_node:IsEmpty() then
|
|
data.IsCenterAvailable = false
|
|
end
|
|
|
|
data.IsSidesAvailable = true
|
|
if bit32.band(dst_node_flags, ImGuiDockNodeFlags.NoDockingSplit) ~= 0 or g.IO.ConfigDockingNoSplit then
|
|
data.IsSidesAvailable = false
|
|
elseif not is_outer_docking and host_node and host_node.ParentNode == nil and host_node:IsCentralNode() then
|
|
data.IsSidesAvailable = false
|
|
elseif bit32.band(src_node_flags, ImGuiDockNodeFlags.NoDockingSplitOther) ~= 0 then
|
|
data.IsSidesAvailable = false
|
|
end
|
|
|
|
-- Build a tentative future node (reuse same structure because it is practical. Shape will be readjusted when previewing a split)
|
|
data.FutureNode.HasCloseButton = (host_node and host_node.HasCloseButton or (not host_node and host_window.HasCloseButton)) or payload_window.HasCloseButton
|
|
data.FutureNode.HasWindowMenuButton = host_node and true or (bit32.band(host_window.Flags, ImGuiWindowFlags.NoCollapse) == 0)
|
|
local rp = ref_node_for_rect and ref_node_for_rect.Pos or host_window.Pos
|
|
local rs = ref_node_for_rect and ref_node_for_rect.Size or host_window.Size
|
|
data.FutureNode.Pos = ImVec2(rp.x, rp.y)
|
|
data.FutureNode.Size = ImVec2(rs.x, rs.y)
|
|
|
|
-- Calculate drop shapes geometry for allowed splitting directions
|
|
data.SplitNode = host_node
|
|
data.SplitDir = ImGuiDir.None
|
|
data.IsSplitDirExplicit = false
|
|
if not host_window.Collapsed then
|
|
for dir = ImGuiDir.None, ImGuiDir.COUNT - 1 do
|
|
if not ((dir == ImGuiDir.None and not data.IsCenterAvailable) or (dir ~= ImGuiDir.None and not data.IsSidesAvailable)) then
|
|
local hit, r = ImGui.DockNodeCalcDropRectsAndTestMousePos(data.FutureNode:Rect(), dir, is_outer_docking, g.IO.MousePos)
|
|
data.DropRectsDraw[dir + 1] = r
|
|
if hit then
|
|
data.SplitDir = dir
|
|
data.IsSplitDirExplicit = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- When docking without holding Shift, we only allow and preview docking when hovering over a drop rect or over the title bar
|
|
data.IsDropAllowed = (data.SplitDir ~= ImGuiDir.None) or data.IsCenterAvailable
|
|
if not is_explicit_target and not data.IsSplitDirExplicit and not g.IO.ConfigDockingWithShift then
|
|
data.IsDropAllowed = false
|
|
end
|
|
|
|
-- Calculate split area
|
|
data.SplitRatio = 0.0
|
|
if data.SplitDir ~= ImGuiDir.None then
|
|
local split_dir = data.SplitDir
|
|
local split_axis = (split_dir == ImGuiDir.Left or split_dir == ImGuiDir.Right) and ImGuiAxis.X or ImGuiAxis.Y
|
|
local pos_new, pos_old = ImVec2(0, 0), ImVec2(data.FutureNode.Pos.x, data.FutureNode.Pos.y)
|
|
local size_new, size_old = ImVec2(0, 0), ImVec2(data.FutureNode.Size.x, data.FutureNode.Size.y)
|
|
ImGui.DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, split_dir, payload_window.Size)
|
|
|
|
-- Calculate split ratio so we can pass it down the docking request
|
|
local split_ratio = ImSaturate(size_new[split_axis] / data.FutureNode.Size[split_axis])
|
|
data.FutureNode.Pos = pos_new
|
|
data.FutureNode.Size = size_new
|
|
data.SplitRatio = (split_dir == ImGuiDir.Right or split_dir == ImGuiDir.Down) and (1.0 - split_ratio) or split_ratio
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodePreviewDockRender(host_window, host_node, root_payload, data)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(g.CurrentWindow == host_window) -- Because we rely on font size to calculate tab sizes
|
|
|
|
-- With this option, we only display the preview on the target viewport, and the payload viewport is made transparent.
|
|
-- To compensate for the single layer obstructed by the payload, we'll increase the alpha of the preview nodes.
|
|
local is_transparent_payload = g.IO.ConfigDockingTransparentPayload
|
|
|
|
local overlay_draw_lists = { ImGui.GetForegroundDrawList(host_window.Viewport) }
|
|
|
|
-- Draw main preview rectangle
|
|
local overlay_col_main = ImGui.GetColorU32(ImGuiCol.DockingPreview, is_transparent_payload and 0.60 or 0.40)
|
|
local overlay_col_drop = ImGui.GetColorU32(ImGuiCol.DockingPreview, is_transparent_payload and 0.90 or 0.70)
|
|
local overlay_col_drop_hovered = ImGui.GetColorU32(ImGuiCol.DockingPreview, is_transparent_payload and 1.20 or 1.00)
|
|
local overlay_col_lines = ImGui.GetColorU32(ImGuiCol.NavWindowingHighlight, is_transparent_payload and 0.80 or 0.60)
|
|
|
|
-- Display area preview
|
|
local can_preview_tabs = (root_payload.DockNodeAsHost == nil or root_payload.DockNodeAsHost.Windows.Size > 0)
|
|
if data.IsDropAllowed then
|
|
local overlay_rect = data.FutureNode:Rect()
|
|
if data.SplitDir == ImGuiDir.None and can_preview_tabs then
|
|
overlay_rect.Min.y = overlay_rect.Min.y + ImGui.GetFrameHeight()
|
|
end
|
|
if data.SplitDir ~= ImGuiDir.None or data.IsCenterAvailable then
|
|
for _, dl in ipairs(overlay_draw_lists) do
|
|
dl:AddRectFilled(overlay_rect.Min, overlay_rect.Max, overlay_col_main, host_window.WindowRounding, ImGui.CalcRoundingFlagsForRectInRect(overlay_rect, host_window:Rect(), g.Style.DockingSeparatorSize))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Display tab shape/label preview unless we are splitting node (it generally makes the situation harder to read)
|
|
if data.IsDropAllowed and can_preview_tabs and data.SplitDir == ImGuiDir.None and data.IsCenterAvailable then
|
|
-- Compute target tab bar geometry so we can locate our preview tabs
|
|
local _, tab_bar_rect = ImGui.DockNodeCalcTabBarLayout(data.FutureNode)
|
|
local tab_pos = ImVec2(tab_bar_rect.Min.x, tab_bar_rect.Min.y)
|
|
if host_node and host_node.TabBar then
|
|
if not host_node:IsHiddenTabBar() and not host_node:IsNoTabBar() then
|
|
tab_pos.x = tab_pos.x + host_node.TabBar.WidthAllTabs + g.Style.ItemInnerSpacing.x -- We don't use OffsetNewTab because when using non-persistent-order tab bar it is incremented with each Tab submission.
|
|
else
|
|
tab_pos.x = tab_pos.x + g.Style.ItemInnerSpacing.x + ImGui.TabItemCalcSize(host_node.Windows.Data[1]).x
|
|
end
|
|
elseif bit32.band(host_window.Flags, ImGuiWindowFlags.DockNodeHost) == 0 then
|
|
tab_pos.x = tab_pos.x + g.Style.ItemInnerSpacing.x + ImGui.TabItemCalcSize(host_window).x -- Account for slight offset which will be added when changing from title bar to tab bar
|
|
end
|
|
|
|
-- Draw tab shape/label preview (payload may be a loose window or a host window carrying multiple tabbed windows)
|
|
if root_payload.DockNodeAsHost then
|
|
IM_ASSERT(root_payload.DockNodeAsHost.Windows.Size <= root_payload.DockNodeAsHost.TabBar.Tabs.Size)
|
|
end
|
|
local tab_bar_with_payload = root_payload.DockNodeAsHost and root_payload.DockNodeAsHost.TabBar or nil
|
|
local payload_count = tab_bar_with_payload and tab_bar_with_payload.Tabs.Size or 1
|
|
for payload_n = 1, payload_count do
|
|
-- DockNode's TabBar may have non-window Tabs manually appended by user
|
|
local payload_window = tab_bar_with_payload and tab_bar_with_payload.Tabs.Data[payload_n].Window or root_payload
|
|
if not (tab_bar_with_payload and payload_window == nil) and DockNodeIsDropAllowedOne(payload_window, host_window) then
|
|
-- Calculate the tab bounding box for each payload window
|
|
local tab_size = ImGui.TabItemCalcSize(payload_window)
|
|
local tab_bb = ImRect(tab_pos.x, tab_pos.y, tab_pos.x + tab_size.x, tab_pos.y + tab_size.y)
|
|
tab_pos.x = tab_pos.x + tab_size.x + g.Style.ItemInnerSpacing.x
|
|
local overlay_col_text = ImGui.GetColorU32(payload_window.DockStyle.Colors[ImGuiWindowDockStyleCol.Text], 1.0, true)
|
|
local overlay_col_tabs = ImGui.GetColorU32(payload_window.DockStyle.Colors[ImGuiWindowDockStyleCol.TabSelected], 1.0, true)
|
|
local overlay_col_unsaved_marker = ImGui.GetColorU32(payload_window.DockStyle.Colors[ImGuiWindowDockStyleCol.UnsavedMarker], 1.0, true)
|
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.ColorConvertU32ToFloat4(overlay_col_text))
|
|
ImGui.PushStyleColor(ImGuiCol.UnsavedMarker, ImGui.ColorConvertU32ToFloat4(overlay_col_unsaved_marker))
|
|
for _, dl in ipairs(overlay_draw_lists) do
|
|
local tab_flags = bit32.band(payload_window.Flags, ImGuiWindowFlags.UnsavedDocument) ~= 0 and ImGuiTabItemFlags.UnsavedDocument or 0
|
|
local contains = tab_bar_rect:Contains(tab_bb)
|
|
if not contains then dl:PushClipRect(tab_bar_rect.Min, tab_bar_rect.Max) end
|
|
ImGui.TabItemBackground(dl, tab_bb, tab_flags, overlay_col_tabs)
|
|
ImGui.TabItemLabelAndCloseButton(dl, tab_bb, tab_flags, g.Style.FramePadding, payload_window.Name, 0, 0, false)
|
|
if not contains then dl:PopClipRect() end
|
|
end
|
|
ImGui.PopStyleColor(2)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Display drop boxes
|
|
local scale = g.Style._MainScale or 1.0 -- FIXME-DPI
|
|
local overlay_rounding = ImMax(math.floor(3.0 * scale), g.Style.FrameRounding)
|
|
for dir = ImGuiDir.None, ImGuiDir.COUNT - 1 do
|
|
local draw_r = data.DropRectsDraw[dir + 1]
|
|
if not IsRectInverted(draw_r) then
|
|
local draw_r_in = ImRect(draw_r.Min, draw_r.Max)
|
|
draw_r_in:Expand(math.floor(-2.0 * scale))
|
|
local overlay_col = (data.SplitDir == dir and data.IsSplitDirExplicit) and overlay_col_drop_hovered or overlay_col_drop
|
|
local thickness = math.floor(1.0 * scale)
|
|
for _, dl in ipairs(overlay_draw_lists) do
|
|
local cc = draw_r_in:GetCenter()
|
|
local center = ImVec2(math.floor(cc.x), math.floor(cc.y))
|
|
dl:AddRectFilled(draw_r.Min, draw_r.Max, overlay_col, overlay_rounding)
|
|
dl:AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding, thickness)
|
|
if dir == ImGuiDir.Left or dir == ImGuiDir.Right then
|
|
dl:AddLineV(center.x, draw_r_in.Min.y, draw_r_in.Max.y, overlay_col_lines, thickness)
|
|
end
|
|
if dir == ImGuiDir.Up or dir == ImGuiDir.Down then
|
|
dl:AddLineH(draw_r_in.Min.x, draw_r_in.Max.x, center.y, overlay_col_lines, thickness)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Stop after ImGuiDir.None
|
|
if (host_node and bit32.band(host_node.MergedFlags, ImGuiDockNodeFlags.NoDockingSplit) ~= 0) or g.IO.ConfigDockingNoSplit then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: ImGuiDockNode Tree manipulation functions
|
|
-------------------------------------------------------------------------------
|
|
-- - DockNodeTreeSplit()
|
|
-- - DockNodeTreeMerge()
|
|
-- - DockNodeTreeUpdatePosSize()
|
|
-- - DockNodeTreeUpdateSplitterFindTouchingNode()
|
|
-- - DockNodeTreeUpdateSplitter()
|
|
-- - DockNodeTreeFindFallbackLeafNode()
|
|
-- - DockNodeTreeFindNodeByPos()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.DockNodeTreeSplit(ctx, parent_node, split_axis, split_inheritor_child_idx, split_ratio, new_node)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(split_axis ~= ImGuiAxis.None)
|
|
|
|
local child_0 = (new_node and split_inheritor_child_idx ~= 0) and new_node or ImGui.DockContextAddNode(ctx, 0)
|
|
child_0.ParentNode = parent_node
|
|
|
|
local child_1 = (new_node and split_inheritor_child_idx ~= 1) and new_node or ImGui.DockContextAddNode(ctx, 0)
|
|
child_1.ParentNode = parent_node
|
|
|
|
local child_inheritor = (split_inheritor_child_idx == 0) and child_0 or child_1
|
|
ImGui.DockNodeMoveChildNodes(child_inheritor, parent_node)
|
|
parent_node.ChildNodes[0] = child_0
|
|
parent_node.ChildNodes[1] = child_1
|
|
parent_node.ChildNodes[split_inheritor_child_idx].VisibleWindow = parent_node.VisibleWindow
|
|
parent_node.SplitAxis = split_axis
|
|
parent_node.VisibleWindow = nil
|
|
parent_node.AuthorityForPos = ImGuiDataAuthority.DockNode
|
|
parent_node.AuthorityForSize = ImGuiDataAuthority.DockNode
|
|
|
|
local a = split_axis
|
|
local size_avail = parent_node.Size[a] - g.Style.DockingSeparatorSize
|
|
size_avail = ImMax(size_avail, g.Style.WindowMinSize[a] * 2.0)
|
|
IM_ASSERT(size_avail > 0.0) -- If you created a node manually with DockBuilderAddNode(), you need to also call DockBuilderSetNodeSize() before splitting.
|
|
child_0.SizeRef = ImVec2(parent_node.Size.x, parent_node.Size.y)
|
|
child_1.SizeRef = ImVec2(parent_node.Size.x, parent_node.Size.y)
|
|
child_0.SizeRef[a] = ImTrunc(size_avail * split_ratio)
|
|
child_1.SizeRef[a] = ImTrunc(size_avail - child_0.SizeRef[a])
|
|
|
|
ImGui.DockNodeMoveWindows(parent_node.ChildNodes[split_inheritor_child_idx], parent_node)
|
|
DockSettingsRenameNodeReferences(parent_node.ID, parent_node.ChildNodes[split_inheritor_child_idx].ID)
|
|
ImGui.DockNodeUpdateHasCentralNodeChild(ImGui.DockNodeGetRootNode(parent_node))
|
|
ImGui.DockNodeTreeUpdatePosSize(parent_node, parent_node.Pos, parent_node.Size)
|
|
|
|
-- Flags transfer (e.g. this is where we transfer the ImGuiDockNodeFlags.CentralNode property)
|
|
child_0.SharedFlags = bit32.band(parent_node.SharedFlags, ImGuiDockNodeFlags.SharedFlagsInheritMask_)
|
|
child_1.SharedFlags = bit32.band(parent_node.SharedFlags, ImGuiDockNodeFlags.SharedFlagsInheritMask_)
|
|
child_inheritor.LocalFlags = bit32.band(parent_node.LocalFlags, ImGuiDockNodeFlags.LocalFlagsTransferMask_)
|
|
parent_node.LocalFlags = bit32.band(parent_node.LocalFlags, bit32.bnot(ImGuiDockNodeFlags.LocalFlagsTransferMask_))
|
|
child_0:UpdateMergedFlags()
|
|
child_1:UpdateMergedFlags()
|
|
parent_node:UpdateMergedFlags()
|
|
if child_inheritor:IsCentralNode() then
|
|
ImGui.DockNodeGetRootNode(parent_node).CentralNode = child_inheritor
|
|
end
|
|
end
|
|
|
|
function ImGui.DockNodeTreeMerge(ctx, parent_node, merge_lead_child)
|
|
-- When called from DockContextProcessUndockNode() it is possible that one of the child is nil.
|
|
local child_0 = parent_node.ChildNodes[0]
|
|
local child_1 = parent_node.ChildNodes[1]
|
|
IM_ASSERT(child_0 or child_1)
|
|
IM_ASSERT(merge_lead_child == child_0 or merge_lead_child == child_1)
|
|
if (child_0 and child_0.Windows.Size > 0) or (child_1 and child_1.Windows.Size > 0) then
|
|
IM_ASSERT(parent_node.TabBar == nil)
|
|
IM_ASSERT(parent_node.Windows.Size == 0)
|
|
end
|
|
|
|
local backup_last_explicit_size = ImVec2(parent_node.SizeRef.x, parent_node.SizeRef.y)
|
|
ImGui.DockNodeMoveChildNodes(parent_node, merge_lead_child)
|
|
if child_0 then
|
|
ImGui.DockNodeMoveWindows(parent_node, child_0) -- Generally only 1 of the 2 child node will have windows
|
|
DockSettingsRenameNodeReferences(child_0.ID, parent_node.ID)
|
|
end
|
|
if child_1 then
|
|
ImGui.DockNodeMoveWindows(parent_node, child_1)
|
|
DockSettingsRenameNodeReferences(child_1.ID, parent_node.ID)
|
|
end
|
|
ImGui.DockNodeApplyPosSizeToWindows(parent_node)
|
|
parent_node.AuthorityForPos = ImGuiDataAuthority.Auto
|
|
parent_node.AuthorityForSize = ImGuiDataAuthority.Auto
|
|
parent_node.AuthorityForViewport = ImGuiDataAuthority.Auto
|
|
parent_node.VisibleWindow = merge_lead_child.VisibleWindow
|
|
parent_node.SizeRef = backup_last_explicit_size
|
|
|
|
-- Flags transfer
|
|
local M = ImGuiDockNodeFlags.LocalFlagsTransferMask_
|
|
parent_node.LocalFlags = bit32.band(parent_node.LocalFlags, bit32.bnot(M)) -- Preserve Dockspace flag
|
|
parent_node.LocalFlags = bit32.bor(parent_node.LocalFlags, bit32.band(child_0 and child_0.LocalFlags or 0, M))
|
|
parent_node.LocalFlags = bit32.bor(parent_node.LocalFlags, bit32.band(child_1 and child_1.LocalFlags or 0, M))
|
|
parent_node.LocalFlagsInWindows = bit32.bor(child_0 and child_0.LocalFlagsInWindows or 0, child_1 and child_1.LocalFlagsInWindows or 0) -- FIXME: Would be more consistent to update from actual windows
|
|
parent_node:UpdateMergedFlags()
|
|
|
|
if child_0 then ImGui.DockContextDeleteNode(ctx, child_0) end
|
|
if child_1 then ImGui.DockContextDeleteNode(ctx, child_1) end
|
|
end
|
|
|
|
-- Update Pos/Size for a node hierarchy (don't affect child Windows yet)
|
|
-- (Depth-first, Pre-Order)
|
|
function ImGui.DockNodeTreeUpdatePosSize(node, pos, size, only_write_to_single_node)
|
|
-- During the regular dock node update we write to all nodes.
|
|
-- 'only_write_to_single_node' is only set when turning a node visible mid-frame and we need its size right-away.
|
|
local g = ImGui.GetCurrentContext()
|
|
local write_to_node = only_write_to_single_node == nil or only_write_to_single_node == node
|
|
if write_to_node then
|
|
node.Pos = ImVec2(pos.x, pos.y)
|
|
node.Size = ImVec2(size.x, size.y)
|
|
end
|
|
|
|
if node:IsLeafNode() then return end
|
|
|
|
local child_0 = node.ChildNodes[0]
|
|
local child_1 = node.ChildNodes[1]
|
|
local child_0_pos, child_1_pos = ImVec2(pos.x, pos.y), ImVec2(pos.x, pos.y)
|
|
local child_0_size, child_1_size = ImVec2(size.x, size.y), ImVec2(size.x, size.y)
|
|
|
|
local child_0_is_toward_single_node = (only_write_to_single_node ~= nil and ImGui.DockNodeIsInHierarchyOf(only_write_to_single_node, child_0))
|
|
local child_1_is_toward_single_node = (only_write_to_single_node ~= nil and ImGui.DockNodeIsInHierarchyOf(only_write_to_single_node, child_1))
|
|
local child_0_is_or_will_be_visible = child_0.IsVisible or child_0_is_toward_single_node
|
|
local child_1_is_or_will_be_visible = child_1.IsVisible or child_1_is_toward_single_node
|
|
|
|
if child_0_is_or_will_be_visible and child_1_is_or_will_be_visible then
|
|
local spacing = g.Style.DockingSeparatorSize
|
|
local a = node.SplitAxis
|
|
local size_avail = ImMax(size[a] - spacing, 0.0)
|
|
|
|
-- Size allocation policy
|
|
-- 1) The first 0..WindowMinSize[axis]*2 are allocated evenly to both windows.
|
|
local size_min_each = ImTrunc(ImMin(size_avail, g.Style.WindowMinSize[a] * 2.0) * 0.5)
|
|
|
|
-- FIXME: Blocks 2) and 3) are essentially doing nearly the same thing.
|
|
-- Difference are: write-back to SizeRef; application of a minimum size; rounding before ImTrunc()
|
|
-- Clarify and rework differences between Size & SizeRef and purpose of WantLockSizeOnce
|
|
|
|
-- 2) Process locked absolute size (during a splitter resize we preserve the child of nodes not touching the splitter edge)
|
|
if child_0.WantLockSizeOnce and not child_1.WantLockSizeOnce then
|
|
child_0_size[a] = ImMin(size_avail - 1.0, child_0.Size[a]); child_0.SizeRef[a] = child_0_size[a]
|
|
child_1_size[a] = size_avail - child_0_size[a]; child_1.SizeRef[a] = child_1_size[a]
|
|
elseif child_1.WantLockSizeOnce and not child_0.WantLockSizeOnce then
|
|
child_1_size[a] = ImMin(size_avail - 1.0, child_1.Size[a]); child_1.SizeRef[a] = child_1_size[a]
|
|
child_0_size[a] = size_avail - child_1_size[a]; child_0.SizeRef[a] = child_0_size[a]
|
|
elseif child_0.WantLockSizeOnce and child_1.WantLockSizeOnce then
|
|
-- FIXME-DOCK: We cannot honor the requested size, so apply ratio.
|
|
-- Currently this path will only be taken if code programmatically sets WantLockSizeOnce
|
|
local split_ratio = child_0_size[a] / (child_0_size[a] + child_1_size[a])
|
|
child_0_size[a] = ImTrunc(size_avail * split_ratio); child_0.SizeRef[a] = child_0_size[a]
|
|
child_1_size[a] = size_avail - child_0_size[a]; child_1.SizeRef[a] = child_1_size[a]
|
|
|
|
-- 3) If one window is the central node (~ use remaining space, should be made explicit!), use explicit size from the other, and remainder for the central node
|
|
elseif child_0.SizeRef[a] ~= 0.0 and child_1.HasCentralNodeChild then
|
|
child_0_size[a] = ImMin(size_avail - size_min_each, child_0.SizeRef[a])
|
|
child_1_size[a] = size_avail - child_0_size[a]
|
|
elseif child_1.SizeRef[a] ~= 0.0 and child_0.HasCentralNodeChild then
|
|
child_1_size[a] = ImMin(size_avail - size_min_each, child_1.SizeRef[a])
|
|
child_0_size[a] = size_avail - child_1_size[a]
|
|
else
|
|
-- 4) Otherwise distribute according to the relative ratio of each SizeRef value
|
|
local split_ratio = child_0.SizeRef[a] / (child_0.SizeRef[a] + child_1.SizeRef[a])
|
|
child_0_size[a] = ImMax(size_min_each, ImTrunc(size_avail * split_ratio + 0.5))
|
|
child_1_size[a] = size_avail - child_0_size[a]
|
|
end
|
|
|
|
child_1_pos[a] = child_1_pos[a] + spacing + child_0_size[a]
|
|
end
|
|
|
|
if only_write_to_single_node == nil then
|
|
child_0.WantLockSizeOnce = false
|
|
child_1.WantLockSizeOnce = false
|
|
end
|
|
local child_0_recurse = only_write_to_single_node and child_0_is_toward_single_node or (not only_write_to_single_node and child_0.IsVisible)
|
|
local child_1_recurse = only_write_to_single_node and child_1_is_toward_single_node or (not only_write_to_single_node and child_1.IsVisible)
|
|
if child_0_recurse then ImGui.DockNodeTreeUpdatePosSize(child_0, child_0_pos, child_0_size) end
|
|
if child_1_recurse then ImGui.DockNodeTreeUpdatePosSize(child_1, child_1_pos, child_1_size) end
|
|
end
|
|
|
|
local function DockNodeTreeUpdateSplitterFindTouchingNode(node, axis, side, touching_nodes)
|
|
if node:IsLeafNode() then
|
|
table.insert(touching_nodes, node)
|
|
return
|
|
end
|
|
if node.ChildNodes[0].IsVisible then
|
|
if node.SplitAxis ~= axis or side == 0 or not node.ChildNodes[1].IsVisible then
|
|
DockNodeTreeUpdateSplitterFindTouchingNode(node.ChildNodes[0], axis, side, touching_nodes)
|
|
end
|
|
end
|
|
if node.ChildNodes[1].IsVisible then
|
|
if node.SplitAxis ~= axis or side == 1 or not node.ChildNodes[0].IsVisible then
|
|
DockNodeTreeUpdateSplitterFindTouchingNode(node.ChildNodes[1], axis, side, touching_nodes)
|
|
end
|
|
end
|
|
end
|
|
|
|
local WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04 -- Reduce visual noise by only highlighting the border after a certain time.
|
|
|
|
-- (Depth-First, Pre-Order)
|
|
function ImGui.DockNodeTreeUpdateSplitter(node)
|
|
if node:IsLeafNode() then return end
|
|
|
|
local g = ImGui.GetCurrentContext()
|
|
|
|
local child_0 = node.ChildNodes[0]
|
|
local child_1 = node.ChildNodes[1]
|
|
if child_0.IsVisible and child_1.IsVisible then
|
|
-- Bounding box of the splitter cover the space between both nodes (w = Spacing, h = Size[xy^1] for when splitting horizontally)
|
|
local axis = node.SplitAxis
|
|
IM_ASSERT(axis ~= ImGuiAxis.None)
|
|
local a, o = axis, 3 - axis -- the port's ImGuiAxis is 1-based (X=1,Y=2): it is directly the ImVec2 index
|
|
local bb = ImRect(ImVec2(child_0.Pos.x, child_0.Pos.y), ImVec2(child_1.Pos.x, child_1.Pos.y))
|
|
bb.Min[a] = bb.Min[a] + child_0.Size[a]
|
|
bb.Max[o] = bb.Max[o] + child_1.Size[o]
|
|
-- if g.IO.KeyCtrl then GetForegroundDrawList(g.CurrentWindow.Viewport):AddRect(bb.Min, bb.Max, IM_COL32(255,0,255,255)) end
|
|
|
|
local merged_flags = bit32.bor(child_0.MergedFlags, child_1.MergedFlags) -- Merged flags for BOTH childs
|
|
local no_resize_axis_flag = (axis == ImGuiAxis.X) and ImGuiDockNodeFlags.NoResizeX or ImGuiDockNodeFlags.NoResizeY
|
|
if bit32.band(merged_flags, ImGuiDockNodeFlags.NoResize) ~= 0 or bit32.band(merged_flags, no_resize_axis_flag) ~= 0 then
|
|
local window = g.CurrentWindow
|
|
window.DrawList:AddRectFilled(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.Separator), g.Style.FrameRounding)
|
|
else
|
|
-- bb.Min[axis] = bb.Min[axis] + 1 -- Display a little inward so highlight doesn't connect with nearby tabs on the neighbor node.
|
|
-- bb.Max[axis] = bb.Max[axis] - 1
|
|
ImGui.PushID(node.ID)
|
|
|
|
-- Find resizing limits by gathering list of nodes that are touching the splitter line.
|
|
local touching_nodes = { [0] = {}, [1] = {} }
|
|
local min_size = g.Style.WindowMinSize[a]
|
|
local resize_limits = { [0] = node.ChildNodes[0].Pos[a] + min_size, [1] = node.ChildNodes[1].Pos[a] + node.ChildNodes[1].Size[a] - min_size }
|
|
|
|
local splitter_id = ImGui.GetID("##Splitter")
|
|
if g.ActiveId == splitter_id then -- Only process when splitter is active
|
|
DockNodeTreeUpdateSplitterFindTouchingNode(child_0, axis, 1, touching_nodes[0])
|
|
DockNodeTreeUpdateSplitterFindTouchingNode(child_1, axis, 0, touching_nodes[1])
|
|
for _, tn in ipairs(touching_nodes[0]) do
|
|
resize_limits[0] = ImMax(resize_limits[0], tn:Rect().Min[a] + min_size)
|
|
end
|
|
for _, tn in ipairs(touching_nodes[1]) do
|
|
resize_limits[1] = ImMin(resize_limits[1], tn:Rect().Max[a] - min_size)
|
|
end
|
|
end
|
|
|
|
-- Use a short delay before highlighting the splitter (and changing the mouse cursor) in order for regular mouse movement to not highlight many splitters
|
|
local cur_size_0 = child_0.Size[a]
|
|
local cur_size_1 = child_1.Size[a]
|
|
local min_size_0 = resize_limits[0] - child_0.Pos[a]
|
|
local min_size_1 = child_1.Pos[a] + child_1.Size[a] - resize_limits[1]
|
|
local bg_col = ImGui.GetColorU32(ImGuiCol.WindowBg)
|
|
local changed
|
|
changed, cur_size_0, cur_size_1 = ImGui.SplitterBehavior(bb, ImGui.GetID("##Splitter"), axis, cur_size_0, cur_size_1, min_size_0, min_size_1, g.WindowsBorderHoverPadding, WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER, bg_col)
|
|
if changed then
|
|
if #touching_nodes[0] > 0 and #touching_nodes[1] > 0 then
|
|
child_0.Size[a] = cur_size_0; child_0.SizeRef[a] = cur_size_0
|
|
child_1.Pos[a] = child_1.Pos[a] - (cur_size_1 - child_1.Size[a])
|
|
child_1.Size[a] = cur_size_1; child_1.SizeRef[a] = cur_size_1
|
|
|
|
-- Lock the size of every node that is a sibling of the node we are touching
|
|
-- This might be less desirable if we can merge sibling of a same axis into the same parental level.
|
|
for side_n = 0, 1 do
|
|
for _, touching_node in ipairs(touching_nodes[side_n]) do
|
|
while touching_node.ParentNode ~= node do
|
|
if touching_node.ParentNode.SplitAxis == axis then
|
|
-- Mark other node so its size will be preserved during the upcoming call to DockNodeTreeUpdatePosSize().
|
|
local node_to_preserve = touching_node.ParentNode.ChildNodes[side_n]
|
|
node_to_preserve.WantLockSizeOnce = true
|
|
end
|
|
touching_node = touching_node.ParentNode
|
|
end
|
|
end
|
|
end
|
|
|
|
ImGui.DockNodeTreeUpdatePosSize(child_0, child_0.Pos, child_0.Size)
|
|
ImGui.DockNodeTreeUpdatePosSize(child_1, child_1.Pos, child_1.Size)
|
|
ImGui.MarkIniSettingsDirty()
|
|
end
|
|
end
|
|
ImGui.PopID()
|
|
end
|
|
end
|
|
|
|
if child_0.IsVisible then ImGui.DockNodeTreeUpdateSplitter(child_0) end
|
|
if child_1.IsVisible then ImGui.DockNodeTreeUpdateSplitter(child_1) end
|
|
end
|
|
|
|
function ImGui.DockNodeTreeFindFallbackLeafNode(node)
|
|
if node:IsLeafNode() then return node end
|
|
local leaf_node = ImGui.DockNodeTreeFindFallbackLeafNode(node.ChildNodes[0])
|
|
if leaf_node then return leaf_node end
|
|
leaf_node = ImGui.DockNodeTreeFindFallbackLeafNode(node.ChildNodes[1])
|
|
if leaf_node then return leaf_node end
|
|
return nil
|
|
end
|
|
|
|
function ImGui.DockNodeTreeFindVisibleNodeByPos(node, pos)
|
|
if not node.IsVisible then return nil end
|
|
|
|
local r = ImRect(node.Pos, node.Pos + node.Size)
|
|
if not r:Contains(pos) then return nil end
|
|
if node:IsLeafNode() then return node end
|
|
local hovered_node = ImGui.DockNodeTreeFindVisibleNodeByPos(node.ChildNodes[0], pos)
|
|
if hovered_node then return hovered_node end
|
|
hovered_node = ImGui.DockNodeTreeFindVisibleNodeByPos(node.ChildNodes[1], pos)
|
|
if hovered_node then return hovered_node end
|
|
|
|
-- This means we are hovering over the splitter/spacing of a parent node
|
|
return node
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Public Functions (SetWindowDock, DockSpace, DockSpaceOverViewport)
|
|
-------------------------------------------------------------------------------
|
|
-- - SetWindowDock() [Internal]
|
|
-- - DockSpace()
|
|
-- - DockSpaceOverViewport()
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- [Internal] Called via SetNextWindowDockID()
|
|
function ImGui.SetWindowDock(window, dock_id, cond)
|
|
-- Test condition (NB: bit 0 is always true) and clear flags for next time
|
|
if cond and cond ~= 0 and bit32.band(window.SetWindowDockAllowFlags, cond) == 0 then return end
|
|
window.SetWindowDockAllowFlags = bit32.band(window.SetWindowDockAllowFlags, bit32.bnot(bit32.bor(ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing)))
|
|
|
|
if window.DockId == dock_id then return end
|
|
|
|
-- If the user attempt to set a dock id that is a split node, we'll dig within to find a suitable docking spot
|
|
local g = ImGui.GetCurrentContext()
|
|
local new_node = ImGui.DockContextFindNodeByID(g, dock_id)
|
|
if new_node and new_node:IsSplitNode() then
|
|
-- Policy: Find central node or latest focused node. We first move back to our root node.
|
|
new_node = ImGui.DockNodeGetRootNode(new_node)
|
|
if new_node.CentralNode then
|
|
IM_ASSERT(new_node.CentralNode:IsCentralNode())
|
|
dock_id = new_node.CentralNode.ID
|
|
else
|
|
dock_id = new_node.LastFocusedNodeId
|
|
end
|
|
end
|
|
|
|
if window.DockId == dock_id then return end
|
|
|
|
if window.DockNode then ImGui.DockNodeRemoveWindow(window.DockNode, window, 0) end
|
|
window.DockId = dock_id
|
|
end
|
|
|
|
-- Create an explicit dockspace node within an existing window. Also expose dock node flags and creates a CentralNode by default.
|
|
-- The Central Node is always displayed even when empty and shrink/extend according to the requested size of its neighbors.
|
|
-- DockSpace() needs to be submitted _before_ any window they can host. If you use a dockspace, submit it early in your app.
|
|
-- When ImGuiDockNodeFlags.KeepAliveOnly is set, nothing is submitted in the current window (function may be called from any location).
|
|
function ImGui.DockSpace(dockspace_id, size_arg, flags, window_class)
|
|
if size_arg == nil then size_arg = ImVec2(0, 0) end
|
|
if flags == nil then flags = 0 end
|
|
local g = ImGui.GetCurrentContext()
|
|
local window = ImGui.GetCurrentWindowRead()
|
|
if bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.DockingEnable) == 0 then return 0 end
|
|
|
|
-- Early out if parent window is hidden/collapsed
|
|
-- This is faster but also DockNodeUpdateTabBar() relies on TabBarLayout() running (which won't if SkipItems=true) to set NextSelectedTabId = 0). See #2960.
|
|
-- If for whichever reason this is causing problem we would need to ensure that DockNodeUpdateTabBar() ends up clearing NextSelectedTabId even if SkipItems=true.
|
|
if window.SkipItems then flags = bit32.bor(flags, ImGuiDockNodeFlags.KeepAliveOnly) end
|
|
if bit32.band(flags, ImGuiDockNodeFlags.KeepAliveOnly) == 0 then window = ImGui.GetCurrentWindow() end -- call to set window.WriteAccessed = true;
|
|
|
|
IM_ASSERT(bit32.band(flags, ImGuiDockNodeFlags.DockSpace) == 0) -- Flag is automatically set by DockSpace() as LocalFlags, not SharedFlags!
|
|
IM_ASSERT(bit32.band(flags, ImGuiDockNodeFlags.CentralNode) == 0) -- Flag is automatically set by DockSpace() as LocalFlags, not SharedFlags! (#8145)
|
|
|
|
IM_ASSERT(dockspace_id ~= 0)
|
|
local node = ImGui.DockContextFindNodeByID(g, dockspace_id)
|
|
if node == nil then
|
|
node = ImGui.DockContextAddNode(g, dockspace_id)
|
|
node:SetLocalFlags(ImGuiDockNodeFlags.CentralNode)
|
|
end
|
|
node.SharedFlags = flags
|
|
node.WindowClass = window_class or ImGuiWindowClass()
|
|
|
|
-- When a DockSpace transitioned form implicit to explicit this may be called a second time
|
|
-- It is possible that the node has already been claimed by a docked window which appeared before the DockSpace() node, so we overwrite IsDockSpace again.
|
|
if node.LastFrameActive == g.FrameCount and bit32.band(flags, ImGuiDockNodeFlags.KeepAliveOnly) == 0 then
|
|
IM_ASSERT(node:IsDockSpace() == false, "Cannot call DockSpace() twice a frame with the same ID")
|
|
node:SetLocalFlags(bit32.bor(node.LocalFlags, ImGuiDockNodeFlags.DockSpace))
|
|
return dockspace_id
|
|
end
|
|
node:SetLocalFlags(bit32.bor(node.LocalFlags, ImGuiDockNodeFlags.DockSpace))
|
|
|
|
-- Keep alive mode, this is allow windows docked into this node so stay docked even if they are not visible
|
|
if bit32.band(flags, ImGuiDockNodeFlags.KeepAliveOnly) ~= 0 then
|
|
node.LastFrameAlive = g.FrameCount
|
|
return dockspace_id
|
|
end
|
|
|
|
local content_avail = ImGui.GetContentRegionAvail()
|
|
local size = ImVec2(ImTrunc(size_arg.x), ImTrunc(size_arg.y))
|
|
if size.x <= 0.0 then size.x = ImMax(content_avail.x + size.x, 4.0) end -- Arbitrary minimum child size (0.0f causing too much issues)
|
|
if size.y <= 0.0 then size.y = ImMax(content_avail.y + size.y, 4.0) end
|
|
IM_ASSERT(size.x > 0.0 and size.y > 0.0)
|
|
|
|
node.Pos = ImVec2(window.DC.CursorPos.x, window.DC.CursorPos.y)
|
|
node.Size = ImVec2(size.x, size.y)
|
|
node.SizeRef = ImVec2(size.x, size.y)
|
|
ImGui.SetNextWindowPos(node.Pos)
|
|
ImGui.SetNextWindowSize(node.Size)
|
|
g.NextWindowData.PosUndock = false
|
|
|
|
-- FIXME-DOCK: Why do we need a child window to host a dockspace, could we host it in the existing window?
|
|
-- FIXME-DOCK: What is the reason for not simply calling BeginChild()? (OK to have a reason but should be commented)
|
|
local window_flags = bit32.bor(ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.DockNodeHost)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoSavedSettings, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoCollapse, ImGuiWindowFlags.NoTitleBar)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoScrollbar, ImGuiWindowFlags.NoScrollWithMouse)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoBackground)
|
|
|
|
local title = string.format("%s/DockSpace_%08X", window.Name, dockspace_id)
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, 0.0)
|
|
ImGui.Begin(title, nil, window_flags)
|
|
ImGui.PopStyleVar()
|
|
|
|
local host_window = g.CurrentWindow
|
|
DockNodeSetupHostWindow(node, host_window)
|
|
host_window.ChildId = window:GetID(title)
|
|
node.OnlyNodeWithWindows = nil
|
|
|
|
IM_ASSERT(node:IsRootNode())
|
|
|
|
-- We need to handle the rare case were a central node is missing.
|
|
-- This can happen if the node was first created manually with DockBuilderAddNode() but _without_ the ImGuiDockNodeFlags.Dockspace.
|
|
-- Doing it correctly would set the _CentralNode flags, which would then propagate according to subsequent split.
|
|
-- It would also be ambiguous to attempt to assign a central node while there are split nodes, so we wait until there's a single node remaining.
|
|
-- The specific sub-property of _CentralNode we are interested in recovering here is the "Don't delete when empty" property,
|
|
-- as it doesn't make sense for an empty dockspace to not have this property.
|
|
if node:IsLeafNode() and not node:IsCentralNode() then
|
|
node:SetLocalFlags(bit32.bor(node.LocalFlags, ImGuiDockNodeFlags.CentralNode))
|
|
end
|
|
|
|
-- Update the node
|
|
ImGui.DockNodeUpdate(node)
|
|
|
|
ImGui.End()
|
|
|
|
local bb = ImRect(node.Pos, node.Pos + size)
|
|
ImGui.ItemSize(size)
|
|
ImGui.ItemAdd(bb, dockspace_id, nil, ImGuiItemFlags.NoNav) -- Not a nav point (could be, would need to draw the nav rect and replicate/refactor activation from BeginChild(), but seems like CTRL+Tab works better here?)
|
|
if bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredRect) ~= 0 and ImGui.IsWindowChildOf(g.HoveredWindow, host_window, false, true) then -- To fullfill IsItemHovered(), similar to EndChild()
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredWindow)
|
|
end
|
|
|
|
return dockspace_id
|
|
end
|
|
|
|
-- Tips: Use with ImGuiDockNodeFlags.PassthruCentralNode!
|
|
-- The limitation with this call is that your window won't have a local menu bar, but you can also use BeginMainMenuBar().
|
|
-- Even though we could pass window flags, it would also require the user to be able to call BeginMenuBar() somehow meaning we can't Begin/End in a single function.
|
|
-- If you really want a menu bar inside the same window as the one hosting the dockspace, you will need to copy this code somewhere and tweak it.
|
|
function ImGui.DockSpaceOverViewport(dockspace_id, viewport, dockspace_flags, window_class)
|
|
if dockspace_id == nil then dockspace_id = 0 end
|
|
if dockspace_flags == nil then dockspace_flags = 0 end
|
|
if viewport == nil then viewport = ImGui.GetMainViewport() end
|
|
|
|
-- Submit a window filling the entire viewport
|
|
ImGui.SetNextWindowPos(viewport.WorkPos)
|
|
ImGui.SetNextWindowSize(viewport.WorkSize)
|
|
ImGui.SetNextWindowViewport(viewport.ID)
|
|
|
|
local host_window_flags = 0
|
|
host_window_flags = bit32.bor(host_window_flags, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoCollapse, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoMove, ImGuiWindowFlags.NoDocking)
|
|
host_window_flags = bit32.bor(host_window_flags, ImGuiWindowFlags.NoBringToFrontOnFocus, ImGuiWindowFlags.NoNavFocus)
|
|
if bit32.band(dockspace_flags, ImGuiDockNodeFlags.PassthruCentralNode) ~= 0 then
|
|
host_window_flags = bit32.bor(host_window_flags, ImGuiWindowFlags.NoBackground)
|
|
end
|
|
|
|
-- FIXME-OPT: When using ImGuiDockNodeFlags.KeepAliveOnly with DockSpaceOverViewport() we might be able to spare submitting the window,
|
|
-- since DockSpace() with that flag doesn't need a window. We'd only need to compute the default ID accordingly.
|
|
if bit32.band(dockspace_flags, ImGuiDockNodeFlags.KeepAliveOnly) ~= 0 then
|
|
host_window_flags = bit32.bor(host_window_flags, ImGuiWindowFlags.NoMouseInputs)
|
|
end
|
|
|
|
local label = string.format("WindowOverViewport_%08X", viewport.ID)
|
|
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0.0)
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0.0)
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, ImVec2(0.0, 0.0))
|
|
ImGui.Begin(label, nil, host_window_flags)
|
|
ImGui.PopStyleVar(3)
|
|
|
|
-- Submit the dockspace
|
|
if dockspace_id == 0 then dockspace_id = ImGui.GetID("DockSpace") end
|
|
ImGui.DockSpace(dockspace_id, ImVec2(0.0, 0.0), dockspace_flags, window_class)
|
|
|
|
ImGui.End()
|
|
|
|
return dockspace_id
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Builder Functions
|
|
-------------------------------------------------------------------------------
|
|
-- Very early end-user API to manipulate dock nodes.
|
|
-- Only available in imgui_internal.h. Expect this API to change/break!
|
|
-- It is expected that those functions are all called _before_ the dockspace node submission.
|
|
-------------------------------------------------------------------------------
|
|
-- - DockBuilderDockWindow()
|
|
-- - DockBuilderGetNode()
|
|
-- - DockBuilderSetNodePos()
|
|
-- - DockBuilderSetNodeSize()
|
|
-- - DockBuilderAddNode()
|
|
-- - DockBuilderRemoveNode()
|
|
-- - DockBuilderRemoveNodeChildNodes()
|
|
-- - DockBuilderRemoveNodeDockedWindows()
|
|
-- - DockBuilderSplitNode()
|
|
-- - DockBuilderCopyNodeRec()
|
|
-- - DockBuilderCopyNode()
|
|
-- - DockBuilderCopyWindowSettings()
|
|
-- - DockBuilderCopyDockSpace()
|
|
-- - DockBuilderFinish()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.DockBuilderDockWindow(window_name, node_id)
|
|
-- We don't preserve relative order of multiple docked windows (by clearing DockOrder back to -1)
|
|
local window_id = ImHashStr(window_name)
|
|
local window = ImGui.FindWindowByID(window_id)
|
|
if window then
|
|
-- Apply to created window
|
|
local prev_node_id = window.DockId
|
|
ImGui.SetWindowDock(window, node_id, ImGuiCond.Always)
|
|
if window.DockId ~= prev_node_id then window.DockOrder = -1 end
|
|
else
|
|
-- Apply to settings
|
|
local settings = ImGui.FindWindowSettingsByID(window_id)
|
|
if settings == nil then settings = ImGui.CreateNewWindowSettings(window_name) end
|
|
if settings.DockId ~= node_id then settings.DockOrder = -1 end
|
|
settings.DockId = node_id
|
|
end
|
|
end
|
|
|
|
function ImGui.DockBuilderGetNode(node_id)
|
|
return ImGui.DockContextFindNodeByID(ImGui.GetCurrentContext(), node_id)
|
|
end
|
|
|
|
function ImGui.DockBuilderGetCentralNode(node_id)
|
|
local node = ImGui.DockBuilderGetNode(node_id)
|
|
if not node then return nil end
|
|
return ImGui.DockNodeGetRootNode(node).CentralNode
|
|
end
|
|
|
|
function ImGui.DockBuilderSetNodePos(node_id, pos)
|
|
local node = ImGui.DockContextFindNodeByID(ImGui.GetCurrentContext(), node_id)
|
|
if node == nil then return end
|
|
node.Pos = ImVec2(pos.x, pos.y)
|
|
node.AuthorityForPos = ImGuiDataAuthority.DockNode
|
|
end
|
|
|
|
function ImGui.DockBuilderSetNodeSize(node_id, size)
|
|
local node = ImGui.DockContextFindNodeByID(ImGui.GetCurrentContext(), node_id)
|
|
if node == nil then return end
|
|
IM_ASSERT(size.x > 0.0 and size.y > 0.0)
|
|
node.Size = ImVec2(size.x, size.y)
|
|
node.SizeRef = ImVec2(size.x, size.y)
|
|
node.AuthorityForSize = ImGuiDataAuthority.DockNode
|
|
end
|
|
|
|
-- Make sure to use the ImGuiDockNodeFlags.DockSpace flag to create a dockspace node! Otherwise this will create a floating node!
|
|
-- - Floating node: you can then call DockBuilderSetNodePos()/DockBuilderSetNodeSize() to position and size the floating node.
|
|
-- - Dockspace node: calling DockBuilderSetNodePos() is unnecessary.
|
|
-- - If you intend to split a node immediately after creation using DockBuilderSplitNode(), make sure to call DockBuilderSetNodeSize() beforehand!
|
|
-- For various reason, the splitting code currently needs a base size otherwise space may not be allocated as precisely as you would expect.
|
|
-- - Use (id == 0) to let the system allocate a node identifier.
|
|
-- - Existing node with a same id will be removed.
|
|
function ImGui.DockBuilderAddNode(node_id, flags)
|
|
if node_id == nil then node_id = 0 end
|
|
if flags == nil then flags = 0 end
|
|
local g = ImGui.GetCurrentContext()
|
|
if node_id ~= 0 then ImGui.DockBuilderRemoveNode(node_id) end
|
|
|
|
local node
|
|
if bit32.band(flags, ImGuiDockNodeFlags.DockSpace) ~= 0 then
|
|
ImGui.DockSpace(node_id, ImVec2(0, 0), bit32.bor(bit32.band(flags, bit32.bnot(ImGuiDockNodeFlags.DockSpace)), ImGuiDockNodeFlags.KeepAliveOnly))
|
|
node = ImGui.DockContextFindNodeByID(g, node_id)
|
|
else
|
|
node = ImGui.DockContextAddNode(g, node_id)
|
|
node:SetLocalFlags(flags)
|
|
end
|
|
node.LastFrameAlive = g.FrameCount -- Set this otherwise BeginDocked will undock during the same frame.
|
|
return node.ID
|
|
end
|
|
|
|
function ImGui.DockBuilderRemoveNode(node_id)
|
|
local g = ImGui.GetCurrentContext()
|
|
local node = ImGui.DockContextFindNodeByID(g, node_id)
|
|
if node == nil then return end
|
|
ImGui.DockBuilderRemoveNodeDockedWindows(node_id, true)
|
|
ImGui.DockBuilderRemoveNodeChildNodes(node_id)
|
|
-- Node may have moved or deleted if e.g. any merge happened
|
|
node = ImGui.DockContextFindNodeByID(g, node_id)
|
|
if node == nil then return end
|
|
if node:IsCentralNode() and node.ParentNode then
|
|
node.ParentNode:SetLocalFlags(bit32.bor(node.ParentNode.LocalFlags, ImGuiDockNodeFlags.CentralNode))
|
|
end
|
|
ImGui.DockContextRemoveNode(g, node, true)
|
|
end
|
|
|
|
-- root_id = 0 to remove all, root_id != 0 to remove child of given node.
|
|
function ImGui.DockBuilderRemoveNodeChildNodes(root_id)
|
|
local g = ImGui.GetCurrentContext()
|
|
local dc = g.DockContext
|
|
|
|
local root_node = (root_id ~= 0) and ImGui.DockContextFindNodeByID(g, root_id) or nil
|
|
if root_id ~= 0 and root_node == nil then return end
|
|
local has_central_node = false
|
|
|
|
local backup_root_node_authority_for_pos = root_node and root_node.AuthorityForPos or ImGuiDataAuthority.Auto
|
|
local backup_root_node_authority_for_size = root_node and root_node.AuthorityForSize or ImGuiDataAuthority.Auto
|
|
|
|
-- Process active windows
|
|
local nodes_to_remove = {}
|
|
for _, id in ipairs(DockContextNodesSorted(g)) do
|
|
local node = dc.Nodes[id]
|
|
if node then
|
|
local want_removal = (root_id == 0) or (node.ID ~= root_id and ImGui.DockNodeGetRootNode(node).ID == root_id)
|
|
if want_removal then
|
|
if node:IsCentralNode() then has_central_node = true end
|
|
if root_id ~= 0 then ImGui.DockContextQueueNotifyRemovedNode(g, node) end
|
|
if root_node then
|
|
ImGui.DockNodeMoveWindows(root_node, node)
|
|
DockSettingsRenameNodeReferences(node.ID, root_node.ID)
|
|
end
|
|
table.insert(nodes_to_remove, node)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- DockNodeMoveWindows->DockNodeAddWindow will normally set those when reaching two windows (which is only adequate during interactive merge)
|
|
-- Make sure we don't lose our current pos/size. (FIXME-DOCK: Consider tidying up that code in DockNodeAddWindow instead)
|
|
if root_node then
|
|
root_node.AuthorityForPos = backup_root_node_authority_for_pos
|
|
root_node.AuthorityForSize = backup_root_node_authority_for_size
|
|
end
|
|
|
|
-- Apply to settings
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
local window_settings_dock_id = settings.DockId
|
|
if window_settings_dock_id and window_settings_dock_id ~= 0 then
|
|
for _, n in ipairs(nodes_to_remove) do
|
|
if n.ID == window_settings_dock_id then
|
|
settings.DockId = root_id
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Not really efficient, but easier to destroy a whole hierarchy considering DockContextRemoveNode is attempting to merge nodes
|
|
if #nodes_to_remove > 1 then
|
|
local depth = {}
|
|
for _, n in ipairs(nodes_to_remove) do depth[n] = ImGui.DockNodeGetDepth(n) end
|
|
table.sort(nodes_to_remove, function(a, b) return depth[a] > depth[b] end)
|
|
end
|
|
for _, n in ipairs(nodes_to_remove) do
|
|
ImGui.DockContextRemoveNode(g, n, false)
|
|
end
|
|
|
|
if root_id == 0 then
|
|
dc.Nodes = {}
|
|
dc.Requests:clear()
|
|
elseif has_central_node then
|
|
root_node.CentralNode = root_node
|
|
root_node:SetLocalFlags(bit32.bor(root_node.LocalFlags, ImGuiDockNodeFlags.CentralNode))
|
|
end
|
|
end
|
|
|
|
function ImGui.DockBuilderRemoveNodeDockedWindows(root_id, clear_settings_refs)
|
|
if clear_settings_refs == nil then clear_settings_refs = true end
|
|
-- Clear references in settings
|
|
local g = ImGui.GetCurrentContext()
|
|
if clear_settings_refs then
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
local want_removal = (root_id == 0) or (settings.DockId == root_id)
|
|
if not want_removal and settings.DockId ~= 0 then
|
|
local node = ImGui.DockContextFindNodeByID(g, settings.DockId)
|
|
if node and ImGui.DockNodeGetRootNode(node).ID == root_id then want_removal = true end
|
|
end
|
|
if want_removal then settings.DockId = 0 end
|
|
end
|
|
end
|
|
|
|
-- Clear references in windows
|
|
for n = 1, g.Windows.Size do
|
|
local window = g.Windows.Data[n]
|
|
local want_removal = (root_id == 0) or (window.DockNode and ImGui.DockNodeGetRootNode(window.DockNode).ID == root_id) or (window.DockNodeAsHost and window.DockNodeAsHost.ID == root_id)
|
|
if want_removal then
|
|
local backup_dock_id = window.DockId
|
|
ImGui.DockContextProcessUndockWindow(g, window, clear_settings_refs)
|
|
if not clear_settings_refs then IM_ASSERT(window.DockId == backup_dock_id) end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Returns (id_at_dir, id_at_opposite_dir): the IDs of the two new nodes created.
|
|
-- First return value is ID of the node at the specified direction.
|
|
-- FIXME-DOCK: We are not exposing nor using split_outer.
|
|
--- @return ImGuiID id_at_dir, ImGuiID id_at_opposite_dir
|
|
function ImGui.DockBuilderSplitNode(id, split_dir, size_ratio_for_node_at_dir)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(split_dir ~= ImGuiDir.None)
|
|
|
|
local node = ImGui.DockContextFindNodeByID(g, id)
|
|
if node == nil then
|
|
IM_ASSERT(node ~= nil)
|
|
return 0, 0
|
|
end
|
|
|
|
local req = ImGuiDockRequest()
|
|
req.Type = ImGuiDockRequestType.Split
|
|
req.DockTargetWindow = nil
|
|
req.DockTargetNode = node
|
|
req.DockPayload = nil
|
|
req.DockSplitDir = split_dir
|
|
req.DockSplitRatio = ImSaturate((split_dir == ImGuiDir.Left or split_dir == ImGuiDir.Up) and size_ratio_for_node_at_dir or (1.0 - size_ratio_for_node_at_dir))
|
|
req.DockSplitOuter = false
|
|
ImGui.DockContextProcessDock(g, req)
|
|
|
|
local first = (split_dir == ImGuiDir.Left or split_dir == ImGuiDir.Up)
|
|
local id_at_dir = node.ChildNodes[first and 0 or 1].ID
|
|
local id_at_opposite_dir = node.ChildNodes[first and 1 or 0].ID
|
|
return id_at_dir, id_at_opposite_dir
|
|
end
|
|
|
|
local function DockBuilderCopyNodeRec(src_node, dst_node_id_if_known, out_node_remap_pairs)
|
|
local g = ImGui.GetCurrentContext()
|
|
local dst_node = ImGui.DockContextAddNode(g, dst_node_id_if_known)
|
|
dst_node.SharedFlags = src_node.SharedFlags
|
|
dst_node.LocalFlags = src_node.LocalFlags
|
|
dst_node.LocalFlagsInWindows = ImGuiDockNodeFlags.None
|
|
dst_node.Pos = ImVec2(src_node.Pos.x, src_node.Pos.y)
|
|
dst_node.Size = ImVec2(src_node.Size.x, src_node.Size.y)
|
|
dst_node.SizeRef = ImVec2(src_node.SizeRef.x, src_node.SizeRef.y)
|
|
dst_node.SplitAxis = src_node.SplitAxis
|
|
dst_node:UpdateMergedFlags()
|
|
|
|
table.insert(out_node_remap_pairs, src_node.ID)
|
|
table.insert(out_node_remap_pairs, dst_node.ID)
|
|
|
|
for child_n = 0, 1 do
|
|
if src_node.ChildNodes[child_n] then
|
|
dst_node.ChildNodes[child_n] = DockBuilderCopyNodeRec(src_node.ChildNodes[child_n], 0, out_node_remap_pairs)
|
|
dst_node.ChildNodes[child_n].ParentNode = dst_node
|
|
end
|
|
end
|
|
return dst_node
|
|
end
|
|
|
|
--- @param out_node_remap_pairs table # 1-based array, cleared and filled with (src_id, dst_id) pairs
|
|
function ImGui.DockBuilderCopyNode(src_node_id, dst_node_id, out_node_remap_pairs)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(src_node_id ~= 0)
|
|
IM_ASSERT(dst_node_id ~= 0)
|
|
IM_ASSERT(out_node_remap_pairs ~= nil)
|
|
|
|
ImGui.DockBuilderRemoveNode(dst_node_id)
|
|
|
|
local src_node = ImGui.DockContextFindNodeByID(g, src_node_id)
|
|
IM_ASSERT(src_node ~= nil)
|
|
|
|
for k in pairs(out_node_remap_pairs) do out_node_remap_pairs[k] = nil end
|
|
DockBuilderCopyNodeRec(src_node, dst_node_id, out_node_remap_pairs)
|
|
IM_ASSERT((#out_node_remap_pairs % 2) == 0)
|
|
end
|
|
|
|
function ImGui.DockBuilderCopyWindowSettings(src_name, dst_name)
|
|
local src_window = ImGui.FindWindowByName(src_name)
|
|
if src_window == nil then return end
|
|
local dst_window = ImGui.FindWindowByName(dst_name)
|
|
if dst_window then
|
|
ImVec2_Copy(dst_window.Pos, src_window.Pos)
|
|
ImVec2_Copy(dst_window.Size, src_window.Size)
|
|
ImVec2_Copy(dst_window.SizeFull, src_window.SizeFull)
|
|
dst_window.Collapsed = src_window.Collapsed
|
|
else
|
|
local dst_settings = ImGui.FindWindowSettingsByID(ImHashStr(dst_name))
|
|
if not dst_settings then dst_settings = ImGui.CreateNewWindowSettings(dst_name) end
|
|
local wx, wy = ImTrunc(src_window.Pos.x), ImTrunc(src_window.Pos.y)
|
|
if src_window.ViewportId ~= 0 and src_window.ViewportId ~= 0x11111111 then
|
|
ImVec2_CopyV(dst_settings.ViewportPos, wx, wy)
|
|
dst_settings.ViewportId = src_window.ViewportId
|
|
ImVec2_CopyV(dst_settings.Pos, 0, 0)
|
|
else
|
|
ImVec2_CopyV(dst_settings.Pos, wx, wy)
|
|
end
|
|
ImVec2_CopyV(dst_settings.Size, ImTrunc(src_window.SizeFull.x), ImTrunc(src_window.SizeFull.y))
|
|
dst_settings.Collapsed = src_window.Collapsed
|
|
end
|
|
end
|
|
|
|
-- FIXME: Will probably want to change this signature, in particular how the window remapping pairs are passed.
|
|
--- @param in_window_remap_pairs table # 1-based array of window names: src0, dst0, src1, dst1, ...
|
|
function ImGui.DockBuilderCopyDockSpace(src_dockspace_id, dst_dockspace_id, in_window_remap_pairs)
|
|
IM_ASSERT(src_dockspace_id ~= 0)
|
|
IM_ASSERT(dst_dockspace_id ~= 0)
|
|
IM_ASSERT(in_window_remap_pairs ~= nil)
|
|
IM_ASSERT((#in_window_remap_pairs % 2) == 0)
|
|
|
|
-- Duplicate entire dock
|
|
-- FIXME: When overwriting dst_dockspace_id, windows that aren't part of our dockspace window class but that are docked in a same node will be split apart,
|
|
-- whereas we could attempt to at least keep them together in a new, same floating node.
|
|
local node_remap_pairs = {}
|
|
ImGui.DockBuilderCopyNode(src_dockspace_id, dst_dockspace_id, node_remap_pairs)
|
|
|
|
-- Attempt to transition all the upcoming windows associated to dst_dockspace_id into the newly created hierarchy of dock nodes
|
|
-- (The windows associated to src_dockspace_id are staying in place)
|
|
local src_windows = {}
|
|
for remap_window_n = 1, #in_window_remap_pairs, 2 do
|
|
local src_window_name = in_window_remap_pairs[remap_window_n]
|
|
local dst_window_name = in_window_remap_pairs[remap_window_n + 1]
|
|
local src_window_id = ImHashStr(src_window_name)
|
|
src_windows[src_window_id] = true
|
|
|
|
-- Search in the remapping tables
|
|
local src_dock_id = 0
|
|
local src_window = ImGui.FindWindowByID(src_window_id)
|
|
if src_window then
|
|
src_dock_id = src_window.DockId
|
|
else
|
|
local src_window_settings = ImGui.FindWindowSettingsByID(src_window_id)
|
|
if src_window_settings then src_dock_id = src_window_settings.DockId end
|
|
end
|
|
local dst_dock_id = 0
|
|
for dock_remap_n = 1, #node_remap_pairs, 2 do
|
|
if node_remap_pairs[dock_remap_n] == src_dock_id then
|
|
dst_dock_id = node_remap_pairs[dock_remap_n + 1]
|
|
--node_remap_pairs[dock_remap_n] = node_remap_pairs[dock_remap_n + 1] = 0 -- Clear
|
|
break
|
|
end
|
|
end
|
|
|
|
if dst_dock_id ~= 0 then
|
|
-- Docked windows gets redocked into the new node hierarchy.
|
|
ImGui.DockBuilderDockWindow(dst_window_name, dst_dock_id)
|
|
else
|
|
-- Floating windows gets their settings transferred (regardless of whether the new window already exist or not)
|
|
-- When this is leading to a Copy and not a Move, we would get two overlapping floating windows. Could we possibly dock them together?
|
|
ImGui.DockBuilderCopyWindowSettings(src_window_name, dst_window_name)
|
|
end
|
|
end
|
|
|
|
-- Anything else in the source nodes of 'node_remap_pairs' are windows that are not included in the remapping list.
|
|
-- Find those windows and move to them to the cloned dock node. This may be optional?
|
|
-- Dock those are a second step as undocking would invalidate source dock nodes.
|
|
local dock_remaining_windows = {}
|
|
for dock_remap_n = 1, #node_remap_pairs, 2 do
|
|
local src_dock_id = node_remap_pairs[dock_remap_n]
|
|
if src_dock_id ~= 0 then
|
|
local dst_dock_id = node_remap_pairs[dock_remap_n + 1]
|
|
local node = ImGui.DockBuilderGetNode(src_dock_id)
|
|
for _, window in node.Windows:iter() do
|
|
if not src_windows[window.ID] then
|
|
-- Docked windows gets redocked into the new node hierarchy.
|
|
table.insert(dock_remaining_windows, { Window = window, DockId = dst_dock_id })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
for _, task in ipairs(dock_remaining_windows) do
|
|
ImGui.DockBuilderDockWindow(task.Window.Name, task.DockId)
|
|
end
|
|
end
|
|
|
|
-- FIXME-DOCK: This is awkward because in series of split user is likely to loose access to its root node.
|
|
function ImGui.DockBuilderFinish(root_id)
|
|
--DockContextRebuild(g)
|
|
ImGui.DockContextBuildAddWindowsToNodes(ImGui.GetCurrentContext(), root_id)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Begin/End Support Functions (called from Begin/End)
|
|
-------------------------------------------------------------------------------
|
|
-- - GetWindowAlwaysWantOwnTabBar()
|
|
-- - DockContextBindNodeToWindow()
|
|
-- - BeginDocked()
|
|
-- - BeginDockableDragDropSource()
|
|
-- - BeginDockableDragDropTarget()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.GetWindowAlwaysWantOwnTabBar(window)
|
|
local g = ImGui.GetCurrentContext()
|
|
if g.IO.ConfigDockingAlwaysTabBar or window.WindowClass.DockingAlwaysTabBar then
|
|
if bit32.band(window.Flags, bit32.bor(ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoDocking)) == 0 then
|
|
if not window.IsFallbackWindow then return true end -- We don't support AlwaysTabBar on the fallback/implicit window to avoid unused dock-node overhead/noise
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function ImGui.DockContextBindNodeToWindow(ctx, window)
|
|
local g = ctx
|
|
local node = ImGui.DockContextFindNodeByID(ctx, window.DockId)
|
|
IM_ASSERT(window.DockNode == nil)
|
|
|
|
-- We should not be docking into a split node (SetWindowDock should avoid this)
|
|
if node and node:IsSplitNode() then
|
|
ImGui.DockContextProcessUndockWindow(ctx, window)
|
|
return nil
|
|
end
|
|
|
|
-- Create node
|
|
if node == nil then
|
|
node = ImGui.DockContextAddNode(ctx, window.DockId)
|
|
node.AuthorityForPos = ImGuiDataAuthority.Window
|
|
node.AuthorityForSize = ImGuiDataAuthority.Window
|
|
node.AuthorityForViewport = ImGuiDataAuthority.Window
|
|
node.LastFrameAlive = g.FrameCount
|
|
end
|
|
|
|
-- If the node just turned visible and is part of a hierarchy, it doesn't have a Size assigned by DockNodeTreeUpdatePosSize() yet,
|
|
-- so we're forcing a Pos/Size update from the first ancestor that is already visible (often it will be the root node).
|
|
-- If we don't do this, the window will be assigned a zero-size on its first frame, which won't ideally warm up the layout.
|
|
-- This is a little wonky because we don't normally update the Pos/Size of visible node mid-frame.
|
|
if not node.IsVisible then
|
|
local ancestor_node = node
|
|
while not ancestor_node.IsVisible and ancestor_node.ParentNode do
|
|
ancestor_node = ancestor_node.ParentNode
|
|
end
|
|
IM_ASSERT(ancestor_node.Size.x > 0.0 and ancestor_node.Size.y > 0.0)
|
|
ImGui.DockNodeUpdateHasCentralNodeChild(ImGui.DockNodeGetRootNode(ancestor_node))
|
|
ImGui.DockNodeTreeUpdatePosSize(ancestor_node, ancestor_node.Pos, ancestor_node.Size, node)
|
|
end
|
|
|
|
-- Add window to node
|
|
local node_was_visible = node.IsVisible
|
|
ImGui.DockNodeAddWindow(node, window, true)
|
|
node.IsVisible = node_was_visible -- Don't mark visible right away (so DockContextEndFrame() doesn't render it, maybe other side effects? will see)
|
|
IM_ASSERT(node == window.DockNode)
|
|
return node
|
|
end
|
|
|
|
local function StoreDockStyleForWindow(window)
|
|
local g = ImGui.GetCurrentContext()
|
|
for color_n = 0, ImGuiWindowDockStyleCol.COUNT - 1 do
|
|
window.DockStyle.Colors[color_n] = ImGui.ColorConvertFloat4ToU32(g.Style.Colors[GWindowDockStyleColors[color_n]])
|
|
end
|
|
end
|
|
ImGui.StoreDockStyleForWindow = StoreDockStyleForWindow
|
|
|
|
--- @return bool? p_open
|
|
function ImGui.BeginDocked(window, p_open)
|
|
local g = ImGui.GetCurrentContext()
|
|
|
|
-- Specific extra processing for fallback window (#9151), could be in Begin() as well.
|
|
if window.IsFallbackWindow and not window.WasActive then
|
|
DockNodeHideWindowDuringHostWindowCreation(window)
|
|
return p_open
|
|
end
|
|
|
|
local auto_dock_node = ImGui.GetWindowAlwaysWantOwnTabBar(window)
|
|
if auto_dock_node then
|
|
if window.DockId == 0 then
|
|
IM_ASSERT(window.DockNode == nil)
|
|
window.DockId = ImGui.DockContextGenNodeID(g)
|
|
end
|
|
else
|
|
-- Calling SetNextWindowPos() undock windows by default (by setting PosUndock)
|
|
local want_undock = false
|
|
want_undock = want_undock or bit32.band(window.Flags, ImGuiWindowFlags.NoDocking) ~= 0
|
|
want_undock = want_undock or (bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasPos) ~= 0 and bit32.band(window.SetWindowPosAllowFlags, g.NextWindowData.PosCond) ~= 0 and g.NextWindowData.PosUndock)
|
|
if want_undock then
|
|
ImGui.DockContextProcessUndockWindow(g, window)
|
|
return p_open
|
|
end
|
|
end
|
|
|
|
-- Bind to our dock node
|
|
local node = window.DockNode
|
|
if node ~= nil then IM_ASSERT(window.DockId == node.ID) end
|
|
if window.DockId ~= 0 and node == nil then
|
|
node = ImGui.DockContextBindNodeToWindow(g, window)
|
|
if node == nil then return p_open end
|
|
end
|
|
|
|
-- Undock if our dockspace node disappeared
|
|
-- Note how we are testing for LastFrameAlive and NOT LastFrameActive. A DockSpace node can be maintained alive while being inactive with ImGuiDockNodeFlags.KeepAliveOnly.
|
|
if node.LastFrameAlive < g.FrameCount then
|
|
-- If the window has been orphaned, transition the docknode to an implicit node processed in DockContextNewFrameUpdateDocking()
|
|
local root_node = ImGui.DockNodeGetRootNode(node)
|
|
if root_node.LastFrameAlive < g.FrameCount then
|
|
ImGui.DockContextProcessUndockWindow(g, window)
|
|
else
|
|
window.DockIsActive = true
|
|
end
|
|
return p_open
|
|
end
|
|
|
|
-- Store style overrides
|
|
StoreDockStyleForWindow(window)
|
|
|
|
-- Fast path return. It is common for windows to hold on a persistent DockId but be the only visible window,
|
|
-- and never create neither a host window neither a tab bar.
|
|
-- FIXME-DOCK: replace .HostWindow nil compare with something more explicit (~was initially intended as a first frame test)
|
|
if node.HostWindow == nil then
|
|
if node.State == ImGuiDockNodeState.HostWindowHiddenBecauseWindowsAreResizing then
|
|
window.DockIsActive = true
|
|
end
|
|
if node.Windows.Size > 1 and window.Appearing then -- Only hide appearing window
|
|
DockNodeHideWindowDuringHostWindowCreation(window)
|
|
end
|
|
return p_open
|
|
end
|
|
|
|
-- We can have zero-sized nodes (e.g. children of a small-size dockspace)
|
|
IM_ASSERT(node.HostWindow)
|
|
IM_ASSERT(node:IsLeafNode())
|
|
IM_ASSERT(node.Size.x >= 0.0 and node.Size.y >= 0.0)
|
|
node.State = ImGuiDockNodeState.HostWindowVisible
|
|
|
|
-- Undock if we are submitted earlier than the host window
|
|
if bit32.band(node.MergedFlags, ImGuiDockNodeFlags.KeepAliveOnly) == 0 and window.BeginOrderWithinContext < node.HostWindow.BeginOrderWithinContext then
|
|
ImGui.DockContextProcessUndockWindow(g, window)
|
|
return p_open
|
|
end
|
|
|
|
-- Position/Size window
|
|
ImGui.SetNextWindowPos(node.Pos)
|
|
ImGui.SetNextWindowSize(node.Size)
|
|
g.NextWindowData.PosUndock = false -- Cancel implicit undocking of SetNextWindowPos()
|
|
window.DockIsActive = true
|
|
window.DockNodeIsVisible = true
|
|
window.DockTabIsVisible = false
|
|
if bit32.band(node.MergedFlags, ImGuiDockNodeFlags.KeepAliveOnly) ~= 0 then return p_open end
|
|
|
|
-- When the window is selected we mark it as visible.
|
|
if node.VisibleWindow == window then window.DockTabIsVisible = true end
|
|
|
|
-- Update window flag
|
|
IM_ASSERT(bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0)
|
|
window.Flags = bit32.bor(window.Flags, ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.NoResize)
|
|
window.ChildFlags = bit32.bor(window.ChildFlags, ImGuiChildFlags.AlwaysUseWindowPadding)
|
|
if node:IsHiddenTabBar() or node:IsNoTabBar() then
|
|
window.Flags = bit32.bor(window.Flags, ImGuiWindowFlags.NoTitleBar)
|
|
else
|
|
window.Flags = bit32.band(window.Flags, bit32.bnot(ImGuiWindowFlags.NoTitleBar)) -- Clear the NoTitleBar flag in case the user set it: confusingly enough we need a title bar height so we are correctly offset, but it won't be displayed!
|
|
end
|
|
|
|
-- Save new dock order only if the window has been visible once already
|
|
-- This allows multiple windows to be created in the same frame and have their respective dock orders preserved.
|
|
if node.TabBar and window.WasActive then
|
|
window.DockOrder = ImGui.DockNodeGetTabOrder(window)
|
|
end
|
|
|
|
if (node.WantCloseAll or node.WantCloseTabId == window.TabId) and p_open ~= nil then
|
|
p_open = false
|
|
end
|
|
|
|
-- Update ChildId to allow returning from Child to Parent with Escape
|
|
local parent_window = window.DockNode.HostWindow
|
|
window.ChildId = parent_window:GetID(window.Name)
|
|
return p_open
|
|
end
|
|
|
|
function ImGui.BeginDockableDragDropSource(window)
|
|
local g = ImGui.GetCurrentContext()
|
|
IM_ASSERT(g.ActiveId == window.MoveId)
|
|
IM_ASSERT(g.MovingWindow == window)
|
|
IM_ASSERT(g.CurrentWindow == window)
|
|
|
|
-- 0: Hold SHIFT to disable docking, 1: Hold SHIFT to enable docking.
|
|
if g.IO.ConfigDockingWithShift ~= g.IO.KeyShift then
|
|
-- When ConfigDockingWithShift is set, display a tooltip to increase UI affordance.
|
|
-- We cannot set for HoveredWindowUnderMovingWindow ~= nil here, as it is only valid/useful when drag and drop is already active
|
|
-- (because of the 'is_mouse_dragging_with_an_expected_destination' logic in UpdateViewportsNewFrame() function)
|
|
if g.IO.ConfigDockingWithShift and g.MouseStationaryTimer >= 1.0 and g.ActiveId >= 1 then
|
|
ImGui.SetTooltip("%s", ImGui.LocalizeGetMsg(ImGuiLocKey.DockingHoldShiftToDock))
|
|
end
|
|
return
|
|
end
|
|
|
|
g.LastItemData.ID = window.MoveId
|
|
window = window.RootWindowDockTree
|
|
IM_ASSERT(bit32.band(window.Flags, ImGuiWindowFlags.NoDocking) == 0)
|
|
local is_drag_docking = g.IO.ConfigDockingWithShift or ImRect(0, 0, window.SizeFull.x, ImGui.GetFrameHeight()):Contains(g.ActiveIdClickOffset) -- FIXME-DOCKING: Need to make this stateful and explicit
|
|
local drag_drop_flags = bit32.bor(ImGuiDragDropFlags.SourceNoPreviewTooltip, ImGuiDragDropFlags.SourceNoHoldToOpenOthers, ImGuiDragDropFlags.PayloadAutoExpire, ImGuiDragDropFlags.PayloadNoCrossContext, ImGuiDragDropFlags.PayloadNoCrossProcess)
|
|
if is_drag_docking and ImGui.BeginDragDropSource(drag_drop_flags) then
|
|
ImGui.SetDragDropPayload(IMGUI_PAYLOAD_TYPE_WINDOW, window)
|
|
ImGui.EndDragDropSource()
|
|
StoreDockStyleForWindow(window) -- Store style overrides while dragging (even when not docked) because docking preview may need it.
|
|
end
|
|
end
|
|
|
|
function ImGui.BeginDockableDragDropTarget(window)
|
|
local g = ImGui.GetCurrentContext()
|
|
|
|
--IM_ASSERT(window.RootWindowDockTree == window) -- May also be a DockSpace
|
|
IM_ASSERT(bit32.band(window.Flags, ImGuiWindowFlags.NoDocking) == 0)
|
|
if not g.DragDropActive then return end
|
|
--GetForegroundDrawList(window):AddRect(window.Pos, window.Pos + window.Size, IM_COL32(255, 255, 0, 255))
|
|
if not ImGui.BeginDragDropTargetCustom(window:Rect(), window.ID) then return end
|
|
|
|
-- Peek into the payload before calling AcceptDragDropPayload() so we can handle overlapping dock nodes with filtering
|
|
-- (this is a little unusual pattern, normally most code would call AcceptDragDropPayload directly)
|
|
local payload = g.DragDropPayload
|
|
if not ImGuiPayload_IsDataType(payload, IMGUI_PAYLOAD_TYPE_WINDOW) or not ImGui.DockNodeIsDropAllowed(window, payload.Data) then
|
|
ImGui.EndDragDropTarget()
|
|
return
|
|
end
|
|
|
|
local payload_window = payload.Data
|
|
if ImGui.AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_WINDOW, bit32.bor(ImGuiDragDropFlags.AcceptBeforeDelivery, ImGuiDragDropFlags.AcceptNoDrawDefaultRect)) then
|
|
-- Select target node
|
|
-- (Important: we cannot use g.HoveredDockNode here! Because each of our target node have filters based on payload, each candidate drop target will do its own evaluation)
|
|
local dock_into_floating_window = false
|
|
local node = nil
|
|
if window.DockNodeAsHost then
|
|
-- Cannot assume that node will ~= nil even though we passed the rectangle test: it depends on padding/spacing handled by DockNodeTreeFindVisibleNodeByPos().
|
|
node = ImGui.DockNodeTreeFindVisibleNodeByPos(window.DockNodeAsHost, g.IO.MousePos)
|
|
|
|
-- There is an edge case when docking into a dockspace which only has _inactive_ nodes (because none of the windows are active)
|
|
-- In this case we need to fallback into any leaf mode, possibly the central node.
|
|
-- FIXME-20181220: We should not have to test for IsLeafNode() here but we have another bug to fix first.
|
|
if node and node:IsDockSpace() and node:IsRootNode() then
|
|
node = (node.CentralNode and node:IsLeafNode()) and node.CentralNode or ImGui.DockNodeTreeFindFallbackLeafNode(node)
|
|
end
|
|
else
|
|
if window.DockNode then
|
|
node = window.DockNode
|
|
else
|
|
dock_into_floating_window = true -- Dock into a regular window
|
|
end
|
|
end
|
|
|
|
local explicit_target_rect
|
|
if node and node.TabBar and not node:IsHiddenTabBar() and not node:IsNoTabBar() then
|
|
explicit_target_rect = node.TabBar.BarRect
|
|
else
|
|
explicit_target_rect = ImRect(window.Pos, window.Pos + ImVec2(window.Size.x, ImGui.GetFrameHeight()))
|
|
end
|
|
local is_explicit_target = g.IO.ConfigDockingWithShift or ImGui.IsMouseHoveringRect(explicit_target_rect.Min, explicit_target_rect.Max)
|
|
|
|
-- Preview docking request and find out split direction/ratio
|
|
--local do_preview = true -- Ignore testing for payload:IsPreview() which removes one frame of delay, but breaks overlapping drop targets within the same window.
|
|
local do_preview = ImGuiPayload_IsPreview(payload) or ImGuiPayload_IsDelivery(payload)
|
|
if do_preview and (node ~= nil or dock_into_floating_window) then
|
|
-- If we have a non-leaf node it means we are hovering the border of a parent node, in which case only outer markers will appear.
|
|
local split_inner = ImGuiDockPreviewData()
|
|
local split_outer = ImGuiDockPreviewData()
|
|
local split_data = split_inner
|
|
if node and (node.ParentNode or node:IsCentralNode() or not node:IsLeafNode()) then
|
|
local root_node = ImGui.DockNodeGetRootNode(node)
|
|
if root_node then
|
|
ImGui.DockNodePreviewDockSetup(window, root_node, payload_window, nil, split_outer, is_explicit_target, true)
|
|
if split_outer.IsSplitDirExplicit then split_data = split_outer end
|
|
end
|
|
end
|
|
if not node or node:IsLeafNode() then
|
|
ImGui.DockNodePreviewDockSetup(window, node, payload_window, nil, split_inner, is_explicit_target, false)
|
|
end
|
|
if split_data == split_outer then split_inner.IsDropAllowed = false end
|
|
|
|
-- Draw inner then outer, so that previewed tab (in inner data) will be behind the outer drop boxes
|
|
ImGui.DockNodePreviewDockRender(window, node, payload_window, split_inner)
|
|
ImGui.DockNodePreviewDockRender(window, node, payload_window, split_outer)
|
|
|
|
-- Queue docking request
|
|
if split_data.IsDropAllowed and ImGuiPayload_IsDelivery(payload) then
|
|
ImGui.DockContextQueueDock(g, window, split_data.SplitNode, payload_window, split_data.SplitDir, split_data.SplitRatio, split_data == split_outer)
|
|
end
|
|
end
|
|
end
|
|
ImGui.EndDragDropTarget()
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: Settings
|
|
-------------------------------------------------------------------------------
|
|
-- - DockSettingsRenameNodeReferences()
|
|
-- - DockSettingsRemoveNodeReferences()
|
|
-- - DockSettingsFindNodeSettings()
|
|
-- - DockSettingsHandler_ApplyAll()
|
|
-- - DockSettingsHandler_ReadOpen()
|
|
-- - DockSettingsHandler_ReadLine()
|
|
-- - DockSettingsHandler_DockNodeToSettings()
|
|
-- - DockSettingsHandler_WriteAll()
|
|
-------------------------------------------------------------------------------
|
|
|
|
DockSettingsRenameNodeReferences = function(old_node_id, new_node_id)
|
|
local g = ImGui.GetCurrentContext()
|
|
for _, window in g.Windows:iter() do
|
|
if window.DockId == old_node_id and window.DockNode == nil then window.DockId = new_node_id end
|
|
end
|
|
---- FIXME-OPT: We could remove this loop by storing the index in the map
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
if settings.DockId == old_node_id then settings.DockId = new_node_id end
|
|
end
|
|
end
|
|
|
|
-- Remove references stored in ImGuiWindowSettings to the given ImGuiDockNodeSettings
|
|
--- @param node_ids table # 1-based array
|
|
DockSettingsRemoveNodeReferences = function(node_ids, node_ids_count)
|
|
local g = ImGui.GetCurrentContext()
|
|
local found = 0
|
|
---- FIXME-OPT: We could remove this loop by storing the index in the map
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
for node_n = 1, node_ids_count do
|
|
if settings.DockId == node_ids[node_n] then
|
|
settings.DockId = 0
|
|
settings.DockOrder = -1
|
|
found = found + 1
|
|
if found < node_ids_count then break end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
DockSettingsFindNodeSettings = function(ctx, id)
|
|
-- FIXME-OPT
|
|
for _, s in ctx.DockContext.NodesSettings:iter() do
|
|
if s.ID == id then return s end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Clear settings data
|
|
DockSettingsHandler_ClearAll = function(ctx, handler)
|
|
local dc = ctx.DockContext
|
|
dc.NodesSettings:clear()
|
|
ImGui.DockContextClearNodes(ctx, 0, true)
|
|
end
|
|
|
|
-- Recreate nodes based on settings data
|
|
DockSettingsHandler_ApplyAll = function(ctx, handler)
|
|
-- Prune settings at boot time only
|
|
local dc = ctx.DockContext
|
|
if ctx.Windows.Size == 0 then ImGui.DockContextPruneUnusedSettingsNodes(ctx) end
|
|
ImGui.DockContextBuildNodesFromSettings(ctx, dc.NodesSettings.Data or {}, dc.NodesSettings.Size)
|
|
ImGui.DockContextBuildAddWindowsToNodes(ctx, 0)
|
|
end
|
|
|
|
DockSettingsHandler_ReadOpen = function(ctx, handler, name)
|
|
if name ~= "Data" then return nil end
|
|
return true
|
|
end
|
|
|
|
DockSettingsHandler_ReadLine = function(ctx, handler, entry, line)
|
|
-- Parsing, e.g.
|
|
-- " DockNode ID=0x00000001 Pos=383,193 Size=201,322 Split=Y,0.506 "
|
|
-- " DockNode ID=0x00000002 Parent=0x00000001 "
|
|
-- Important: this code expect currently fields in a fixed order.
|
|
local node = ImGuiDockNodeSettings()
|
|
line = line:gsub("^[ \t]+", "")
|
|
if line:sub(1, 8) == "DockNode" then
|
|
line = line:sub(9)
|
|
elseif line:sub(1, 9) == "DockSpace" then
|
|
line = line:sub(10)
|
|
node.Flags = bit32.bor(node.Flags, ImGuiDockNodeFlags.DockSpace)
|
|
else
|
|
return
|
|
end
|
|
line = line:gsub("^[ \t]+", "")
|
|
local function take(pattern)
|
|
local caps = { line:match("^" .. pattern .. "()") }
|
|
if #caps == 0 then return nil end
|
|
line = line:sub(caps[#caps])
|
|
caps[#caps] = nil
|
|
return table.unpack(caps)
|
|
end
|
|
local v = take("ID=0x(%x+)")
|
|
if not v then return end
|
|
node.ID = tonumber(v, 16)
|
|
v = take(" Parent=0x(%x+)")
|
|
if v then node.ParentNodeId = tonumber(v, 16); if node.ParentNodeId == 0 then return end end
|
|
v = take(" Window=0x(%x+)")
|
|
if v then node.ParentWindowId = tonumber(v, 16); if node.ParentWindowId == 0 then return end end
|
|
if node.ParentNodeId == 0 then
|
|
local x, y = take(" Pos=(%-?%d+),(%-?%d+)")
|
|
if not x then return end
|
|
node.Pos = ImVec2(tonumber(x), tonumber(y))
|
|
x, y = take(" Size=(%-?%d+),(%-?%d+)")
|
|
if not x then return end
|
|
node.Size = ImVec2(tonumber(x), tonumber(y))
|
|
else
|
|
local x, y = take(" SizeRef=(%-?%d+),(%-?%d+)")
|
|
if x then node.SizeRef = ImVec2(tonumber(x), tonumber(y)) end
|
|
end
|
|
local c = take(" Split=(%a)")
|
|
if c then
|
|
if c == "X" then node.SplitAxis = ImGuiAxis.X elseif c == "Y" then node.SplitAxis = ImGuiAxis.Y end
|
|
end
|
|
local function flag(name, f)
|
|
local x = take(" " .. name .. "=(%-?%d+)")
|
|
if x and tonumber(x) ~= 0 then node.Flags = bit32.bor(node.Flags, f) end
|
|
end
|
|
flag("NoResize", ImGuiDockNodeFlags.NoResize)
|
|
flag("CentralNode", ImGuiDockNodeFlags.CentralNode)
|
|
flag("NoTabBar", ImGuiDockNodeFlags.NoTabBar)
|
|
flag("HiddenTabBar", ImGuiDockNodeFlags.HiddenTabBar)
|
|
flag("NoWindowMenuButton", ImGuiDockNodeFlags.NoWindowMenuButton)
|
|
flag("NoCloseButton", ImGuiDockNodeFlags.NoCloseButton)
|
|
v = take(" Selected=0x(%x+)")
|
|
if v then node.SelectedTabId = tonumber(v, 16) end
|
|
if node.ParentNodeId ~= 0 then
|
|
local parent_settings = DockSettingsFindNodeSettings(ctx, node.ParentNodeId)
|
|
if parent_settings then node.Depth = parent_settings.Depth + 1 end
|
|
end
|
|
ctx.DockContext.NodesSettings:push_back(node)
|
|
end
|
|
|
|
local function DockSettingsHandler_DockNodeToSettings(dc, node, depth)
|
|
local node_settings = ImGuiDockNodeSettings()
|
|
node_settings.ID = node.ID
|
|
node_settings.ParentNodeId = node.ParentNode and node.ParentNode.ID or 0
|
|
node_settings.ParentWindowId = (node:IsDockSpace() and node.HostWindow and node.HostWindow.ParentWindow) and node.HostWindow.ParentWindow.ID or 0
|
|
node_settings.SelectedTabId = node.SelectedTabId
|
|
node_settings.SplitAxis = node:IsSplitNode() and node.SplitAxis or ImGuiAxis.None
|
|
node_settings.Depth = depth
|
|
node_settings.Flags = bit32.band(node.LocalFlags, ImGuiDockNodeFlags.SavedFlagsMask_)
|
|
node_settings.Pos = ImVec2(ImTrunc(node.Pos.x), ImTrunc(node.Pos.y))
|
|
node_settings.Size = ImVec2(ImTrunc(node.Size.x), ImTrunc(node.Size.y))
|
|
node_settings.SizeRef = ImVec2(ImTrunc(node.SizeRef.x), ImTrunc(node.SizeRef.y))
|
|
dc.NodesSettings:push_back(node_settings)
|
|
if node.ChildNodes[0] then DockSettingsHandler_DockNodeToSettings(dc, node.ChildNodes[0], depth + 1) end
|
|
if node.ChildNodes[1] then DockSettingsHandler_DockNodeToSettings(dc, node.ChildNodes[1], depth + 1) end
|
|
end
|
|
|
|
DockSettingsHandler_WriteAll = function(ctx, handler, buf)
|
|
local g = ctx
|
|
local dc = ctx.DockContext
|
|
if bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.DockingEnable) == 0 then return end
|
|
|
|
-- Gather settings data
|
|
-- (unlike our windows settings, because nodes are always built we can do a full rewrite of the SettingsNode buffer)
|
|
dc.NodesSettings:resize(0)
|
|
for _, id in ipairs(DockContextNodesSorted(ctx)) do
|
|
local node = dc.Nodes[id]
|
|
if node and node:IsRootNode() then
|
|
DockSettingsHandler_DockNodeToSettings(dc, node, 0)
|
|
end
|
|
end
|
|
|
|
local max_depth = 0
|
|
for _, s in dc.NodesSettings:iter() do max_depth = ImMax(s.Depth, max_depth) end
|
|
|
|
-- Write to text buffer
|
|
buf:appendf("[%s][Data]\n", handler.TypeName)
|
|
for _, s in dc.NodesSettings:iter() do
|
|
buf:append(string.rep(" ", s.Depth * 2) .. ((bit32.band(s.Flags, ImGuiDockNodeFlags.DockSpace) ~= 0) and "DockSpace" or "DockNode ") .. string.rep(" ", (max_depth - s.Depth) * 2)) -- Text align nodes to facilitate looking at .ini file
|
|
buf:appendf(" ID=0x%08X", s.ID)
|
|
if s.ParentNodeId ~= 0 then
|
|
buf:appendf(" Parent=0x%08X SizeRef=%d,%d", s.ParentNodeId, s.SizeRef.x, s.SizeRef.y)
|
|
else
|
|
if s.ParentWindowId ~= 0 then buf:appendf(" Window=0x%08X", s.ParentWindowId) end
|
|
buf:appendf(" Pos=%d,%d Size=%d,%d", s.Pos.x, s.Pos.y, s.Size.x, s.Size.y)
|
|
end
|
|
if s.SplitAxis ~= ImGuiAxis.None then buf:appendf(" Split=%s", (s.SplitAxis == ImGuiAxis.X) and "X" or "Y") end
|
|
if bit32.band(s.Flags, ImGuiDockNodeFlags.NoResize) ~= 0 then buf:append(" NoResize=1") end
|
|
if bit32.band(s.Flags, ImGuiDockNodeFlags.CentralNode) ~= 0 then buf:append(" CentralNode=1") end
|
|
if bit32.band(s.Flags, ImGuiDockNodeFlags.NoTabBar) ~= 0 then buf:append(" NoTabBar=1") end
|
|
if bit32.band(s.Flags, ImGuiDockNodeFlags.HiddenTabBar) ~= 0 then buf:append(" HiddenTabBar=1") end
|
|
if bit32.band(s.Flags, ImGuiDockNodeFlags.NoWindowMenuButton) ~= 0 then buf:append(" NoWindowMenuButton=1") end
|
|
if bit32.band(s.Flags, ImGuiDockNodeFlags.NoCloseButton) ~= 0 then buf:append(" NoCloseButton=1") end
|
|
if s.SelectedTabId ~= 0 then buf:appendf(" Selected=0x%08X", s.SelectedTabId) end
|
|
buf:append("\n")
|
|
end
|
|
buf:append("\n")
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Docking: helpers (imgui_internal.h inlines) + widgets used by docking
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.DockNodeGetRootNode(node)
|
|
while node.ParentNode do node = node.ParentNode end
|
|
return node
|
|
end
|
|
|
|
function ImGui.DockNodeIsInHierarchyOf(node, parent)
|
|
while node do
|
|
if node == parent then return true end
|
|
node = node.ParentNode
|
|
end
|
|
return false
|
|
end
|
|
|
|
function ImGui.DockNodeGetDepth(node)
|
|
local depth = 0
|
|
while node.ParentNode do node = node.ParentNode; depth = depth + 1 end
|
|
return depth
|
|
end
|
|
|
|
function ImGui.DockNodeGetWindowMenuButtonId(node)
|
|
return ImHashStr("#COLLAPSE", nil, node.ID)
|
|
end
|
|
|
|
function ImGui.GetWindowDockID()
|
|
return ImGui.GetCurrentContext().CurrentWindow.DockId
|
|
end
|
|
|
|
function ImGui.IsWindowDocked()
|
|
return ImGui.GetCurrentContext().CurrentWindow.DockIsActive
|
|
end
|
|
|
|
function ImGui.SetNextWindowDockID(id, cond)
|
|
local g = ImGui.GetCurrentContext()
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasDock)
|
|
g.NextWindowData.DockCond = (cond and cond ~= 0) and cond or ImGuiCond.Always
|
|
g.NextWindowData.DockId = id
|
|
end
|
|
|
|
-- We use 'undock == false' when dragging from title bar to allow moving groups of floating nodes without undocking them.
|
|
function ImGui.StartMouseMovingWindowOrNode(window, node, undock)
|
|
local g = ImGui.GetCurrentContext()
|
|
local can_undock_node = false
|
|
if undock and node ~= nil and node.VisibleWindow and bit32.band(node.VisibleWindow.Flags, ImGuiWindowFlags.NoMove) == 0 and bit32.band(node.MergedFlags, ImGuiDockNodeFlags.NoUndocking) == 0 then
|
|
-- Can undock if:
|
|
-- - part of a hierarchy with more than one visible node (if only one is visible, we'll just move the root window)
|
|
-- - part of a dockspace node hierarchy: so we can undock the last single visible node too. Undocking from a fixed/central node will create a new node and copy windows.
|
|
local root_node = ImGui.DockNodeGetRootNode(node)
|
|
if root_node.OnlyNodeWithWindows ~= node or root_node.CentralNode ~= nil then
|
|
can_undock_node = true
|
|
end
|
|
end
|
|
|
|
local clicked = ImGui.IsMouseClicked(0)
|
|
local dragging = ImGui.IsMouseDragging(0)
|
|
if can_undock_node and dragging then
|
|
ImGui.DockContextQueueUndockNode(g, node) -- Will lead to DockNodeStartMouseMovingWindow() -> StartMouseMovingWindow() being called next frame
|
|
elseif not can_undock_node and (clicked or dragging) and g.MovingWindow ~= window then
|
|
ImGui.StartMouseMovingWindow(window)
|
|
end
|
|
end
|
|
|
|
-- This is less wide than RenderArrow() and we use in dock nodes instead of the regular RenderArrow() to denote a change of functionality,
|
|
-- and because the saved space means that the left-most tab label can stay at exactly the same position as the label of a loose window.
|
|
function ImGui.RenderArrowDockMenu(draw_list, p_min, sz, col)
|
|
draw_list:AddRectFilled(p_min + ImVec2(sz * 0.20, sz * 0.15), p_min + ImVec2(sz * 0.80, sz * 0.30), col)
|
|
ImGui.RenderArrowPointingAt(draw_list, p_min + ImVec2(sz * 0.50, sz * 0.85), ImVec2(sz * 0.30, sz * 0.40), ImGuiDir.Down, col)
|
|
end
|
|
|
|
-- Upstream CollapseButton (docking branch): replaces the port's simpler version
|
|
-- The Collapse button also functions as a Dock Menu button.
|
|
function ImGui.CollapseButton(id, pos, dock_node)
|
|
local g = ImGui.GetCurrentContext()
|
|
local window = g.CurrentWindow
|
|
|
|
local bb = ImRect(pos, pos + ImVec2(g.FontSize, g.FontSize))
|
|
local is_clipped = not ImGui.ItemAdd(bb, id)
|
|
local pressed, hovered, held = ImGui.ButtonBehavior(bb, id, ImGuiButtonFlags.None)
|
|
if is_clipped then return pressed end
|
|
|
|
-- Render
|
|
--local is_dock_menu = (window.DockNodeAsHost and not window.Collapsed)
|
|
local bg_col = ImGui.GetColorU32((held and hovered) and ImGuiCol.ButtonActive or (hovered and ImGuiCol.ButtonHovered or ImGuiCol.Button))
|
|
local text_col = ImGui.GetColorU32(ImGuiCol.Text)
|
|
if hovered or held then
|
|
window.DrawList:AddRectFilled(bb.Min, bb.Max, bg_col)
|
|
end
|
|
ImGui.RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags and ImGuiNavRenderCursorFlags.Compact or nil)
|
|
|
|
if dock_node then
|
|
ImGui.RenderArrowDockMenu(window.DrawList, bb.Min, g.FontSize, text_col)
|
|
else
|
|
ImGui.RenderArrow(window.DrawList, bb.Min, text_col, window.Collapsed and ImGuiDir.Right or ImGuiDir.Down, 1.0)
|
|
end
|
|
|
|
-- Switch to moving the window after mouse is moved beyond the initial drag threshold
|
|
if ImGui.IsItemActive() and ImGui.IsMouseDragging(0) then
|
|
ImGui.StartMouseMovingWindowOrNode(window, dock_node, true) -- Undock from window/collapse menu button
|
|
end
|
|
|
|
return pressed
|
|
end
|
|
|
|
-- Using 'hover_visibility_delay' allows us to hide the highlight and mouse cursor for a short time, which can be convenient to reduce visual noise.
|
|
--- @return bool held, float size1, float size2
|
|
function ImGui.SplitterBehavior(bb, id, axis, size1, size2, min_size1, min_size2, hover_extend, hover_visibility_delay, bg_col)
|
|
hover_extend = hover_extend or 0.0
|
|
hover_visibility_delay = hover_visibility_delay or 0.0
|
|
bg_col = bg_col or 0
|
|
local g = ImGui.GetCurrentContext()
|
|
local window = g.CurrentWindow
|
|
|
|
if not ImGui.ItemAdd(bb, id, nil, ImGuiItemFlags.NoNav) then return false, size1, size2 end
|
|
|
|
-- FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags.AllowOverlap here is
|
|
-- to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item.
|
|
-- Nowadays we would instead want to use SetNextItemAllowOverlap() before the item.
|
|
local button_flags = bit32.bor(ImGuiButtonFlags.FlattenChildren, ImGuiButtonFlags.AllowOverlap)
|
|
|
|
local bb_interact = ImRect(bb.Min, bb.Max)
|
|
bb_interact:Expand(axis == ImGuiAxis.Y and ImVec2(0.0, hover_extend) or ImVec2(hover_extend, 0.0))
|
|
local _, hovered, held = ImGui.ButtonBehavior(bb_interact, id, button_flags)
|
|
if hovered then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredRect) -- for IsItemHovered(), because bb_interact is larger than bb
|
|
end
|
|
|
|
if held or (hovered and g.HoveredIdPreviousFrame == id and g.HoveredIdTimer >= hover_visibility_delay) then
|
|
ImGui.SetMouseCursor(axis == ImGuiAxis.Y and ImGuiMouseCursor.ResizeNS or ImGuiMouseCursor.ResizeEW)
|
|
end
|
|
|
|
local bb_render = ImRect(bb.Min, bb.Max)
|
|
if held then
|
|
local d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min
|
|
local mouse_delta = d[axis]
|
|
|
|
-- Minimum pane size
|
|
local size_1_maximum_delta = ImMax(0.0, size1 - min_size1)
|
|
local size_2_maximum_delta = ImMax(0.0, size2 - min_size2)
|
|
if mouse_delta < -size_1_maximum_delta then mouse_delta = -size_1_maximum_delta end
|
|
if mouse_delta > size_2_maximum_delta then mouse_delta = size_2_maximum_delta end
|
|
|
|
-- Apply resize
|
|
if mouse_delta ~= 0.0 then
|
|
size1 = ImMax(size1 + mouse_delta, min_size1)
|
|
size2 = ImMax(size2 - mouse_delta, min_size2)
|
|
bb_render:Translate(axis == ImGuiAxis.X and ImVec2(mouse_delta, 0.0) or ImVec2(0.0, mouse_delta))
|
|
ImGui.MarkItemEdited(id)
|
|
end
|
|
end
|
|
|
|
-- Render at new position
|
|
if bit32.band(bg_col, IM_COL32_A_MASK) ~= 0 then
|
|
window.DrawList:AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0)
|
|
end
|
|
local col = ImGui.GetColorU32(held and ImGuiCol.SeparatorActive or ((hovered and g.HoveredIdTimer >= hover_visibility_delay) and ImGuiCol.SeparatorHovered or ImGuiCol.Separator))
|
|
window.DrawList:AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0)
|
|
|
|
return held, size1, size2
|
|
end
|
|
end --[[ [SECTION] imgui_docking (was imgui_docking.lua) ]]
|
|
|
|
-- Debug options
|
|
local IMGUI_DEBUG_NAV_SCORING = false -- Display navigation scoring preview when hovering items. Hold Ctrl to display for all candidates. Ctrl+Arrow to change last direction.
|
|
local IMGUI_DEBUG_NAV_RECTS = false -- Display the reference navigation rectangle for each window
|
|
|
|
-- Default font size if unspecified in both style.FontSizeBase and AddFontXXX() calls.
|
|
local FONT_DEFAULT_SIZE_BASE = 20
|
|
|
|
local WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04 -- Reduce visual noise by only highlighting the border after a certain time.
|
|
local WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 0.70 -- Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certain time, unless mouse moved.
|
|
|
|
-- Tooltip offset
|
|
local TOOLTIP_DEFAULT_OFFSET_MOUSE = ImVec2(16, 10) -- Multiplied by g.Style.MouseCursorScale
|
|
local TOOLTIP_DEFAULT_OFFSET_TOUCH = ImVec2(0, -20) -- Multiplied by g.Style.MouseCursorScale
|
|
local TOOLTIP_DEFAULT_PIVOT_TOUCH = ImVec2(0.5, 1.0) -- Multiplied by g.Style.MouseCursorScale
|
|
|
|
local IMGUI_VIEWPORT_DEFAULT_ID = 0x11111111 -- Using an arbitrary constant instead of e.g. ImHashStr("ViewportDefault", 0); so it's easier to spot in the debugger. The exact value doesn't matter.
|
|
|
|
local string = string
|
|
ImFormatString = string.format -- TODO: an simplified version that operates on byte tables directly?
|
|
|
|
--- @module "imstd_minstdio"
|
|
ImStd.sscanf = IM_INCLUDE"imstd_minstdio.lua"
|
|
|
|
local math = math
|
|
local bit32 = bit32
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] MISC HELPERS/UTILITIES (File functions)
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- This closes the _file.
|
|
--- @param _file any # File object
|
|
function IM_FILE_CLOSE(_file) end
|
|
|
|
--- This returns the size of _file in bytes.
|
|
--- @param _file any # File object
|
|
--- @return int
|
|
function IM_FILE_SIZE(_file) return -1 end
|
|
|
|
--- This writes _str into _file.
|
|
--- @param _file any # File object
|
|
--- @param _str string
|
|
function IM_FILE_WRITE(_file, _str) end
|
|
|
|
--- This reads the specified _count of chars and returns them as a binary string.
|
|
--- @param _file any # File object
|
|
--- @param _count int
|
|
--- @return string
|
|
function IM_FILE_READ(_file, _count) return "" end
|
|
|
|
--- This opens the file at _filename in _mode and returns the File object.
|
|
--- @param _filename string
|
|
--- @param _mode string # e.g. "rb"
|
|
--- @return any
|
|
function ImStd.ImFileOpen(_filename, _mode) end
|
|
|
|
--- [GMod] Platform specific
|
|
if gmod then
|
|
IM_FILE_CLOSE = FindMetaTable("File").Close --- @type function
|
|
IM_FILE_SIZE = FindMetaTable("File").Size --- @type function
|
|
IM_FILE_WRITE = FindMetaTable("File").Write --- @type function
|
|
IM_FILE_READ = FindMetaTable("File").Read --- @type function
|
|
end
|
|
|
|
-- Default file functions
|
|
if not IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS then
|
|
--- [GMod] Another Platform specific
|
|
if gmod then
|
|
function ImStd.ImFileOpen(filename, mode) return file.Open(filename, mode, "GAME") end
|
|
end
|
|
|
|
function ImStd.ImFileClose(f) IM_FILE_CLOSE(f) end
|
|
function ImStd.ImFileGetSize(f) return IM_FILE_SIZE(f) end
|
|
|
|
--- @param f any # File object
|
|
--- @param data table # 1-based table to store result data
|
|
--- @param count int # Amount of bytes to read from `f`
|
|
function ImStd.ImFileRead(f, data, count)
|
|
local CHUNK_SIZE = 8000 -- We don't read byte by byte
|
|
local offset = 0
|
|
|
|
while offset < count do
|
|
local read_size = math.min(CHUNK_SIZE, count - offset)
|
|
local str = IM_FILE_READ(f, read_size)
|
|
|
|
local bytes = {string.byte(str, 1, read_size)}
|
|
for i = 1, read_size do
|
|
data[offset + i] = bytes[i]
|
|
end
|
|
|
|
offset = offset + read_size
|
|
end
|
|
end
|
|
|
|
-- Helper: Load file content into memory
|
|
-- This can't really be used with "rt" because fseek size won't match read size.
|
|
--- @param filename string
|
|
--- @param mode string
|
|
--- @return table?, integer?
|
|
function ImStd.ImFileLoadToMemory(filename, mode)
|
|
local f = ImStd.ImFileOpen(filename, mode)
|
|
if not f then return end
|
|
|
|
local file_size = ImStd.ImFileGetSize(f)
|
|
if file_size <= 0 then
|
|
ImStd.ImFileClose(f)
|
|
return
|
|
end
|
|
|
|
local file_data = {}
|
|
ImStd.ImFileRead(f, file_data, file_size)
|
|
if #file_data == 0 then
|
|
ImStd.ImFileClose(f)
|
|
return
|
|
end
|
|
|
|
ImStd.ImFileClose(f)
|
|
|
|
return file_data, file_size
|
|
end
|
|
end
|
|
|
|
--- Forward Declarations
|
|
local CalcNextScrollFromScrollTargetAndClamp
|
|
|
|
local MT = ImGui.GetMetatables()
|
|
|
|
-- Scale all spacing/padding/thickness values. Do not scale fonts.
|
|
-- Consider not calling this if your initial scale factor if <1.0.
|
|
-- Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times.
|
|
--- @param scale_factor float
|
|
function MT.ImGuiStyle:ScaleAllSizes(scale_factor)
|
|
self._MainScale = self._MainScale * scale_factor
|
|
|
|
self.WindowPadding = ImTrunc(self.WindowPadding * scale_factor)
|
|
self.WindowRounding = ImTrunc(self.WindowRounding * scale_factor)
|
|
self.WindowBorderSize = ImTrunc(self.WindowBorderSize * scale_factor)
|
|
self.WindowMinSize = ImTrunc(self.WindowMinSize * scale_factor)
|
|
self.WindowBorderHoverPadding = ImTrunc(self.WindowBorderHoverPadding * scale_factor)
|
|
self.ChildRounding = ImTrunc(self.ChildRounding * scale_factor)
|
|
self.ChildBorderSize = ImTrunc(self.ChildBorderSize * scale_factor)
|
|
self.PopupRounding = ImTrunc(self.PopupRounding * scale_factor)
|
|
self.PopupBorderSize = ImTrunc(self.PopupBorderSize * scale_factor)
|
|
self.FramePadding = ImTrunc(self.FramePadding * scale_factor)
|
|
self.FrameBorderSize = ImTrunc(self.FrameBorderSize * scale_factor)
|
|
self.FrameRounding = ImTrunc(self.FrameRounding * scale_factor)
|
|
self.ItemSpacing = ImTrunc(self.ItemSpacing * scale_factor)
|
|
self.ItemInnerSpacing = ImTrunc(self.ItemInnerSpacing * scale_factor)
|
|
self.CellPadding = ImTrunc(self.CellPadding * scale_factor)
|
|
self.TouchExtraPadding = ImTrunc(self.TouchExtraPadding * scale_factor)
|
|
self.IndentSpacing = ImTrunc(self.IndentSpacing * scale_factor)
|
|
self.ColumnsMinSpacing = ImTrunc(self.ColumnsMinSpacing * scale_factor)
|
|
self.ScrollbarSize = ImTrunc(self.ScrollbarSize * scale_factor)
|
|
self.ScrollbarRounding = ImTrunc(self.ScrollbarRounding * scale_factor)
|
|
self.ScrollbarPadding = ImTrunc(self.ScrollbarPadding * scale_factor)
|
|
self.GrabMinSize = ImTrunc(self.GrabMinSize * scale_factor)
|
|
self.GrabRounding = ImTrunc(self.GrabRounding * scale_factor)
|
|
self.LogSliderDeadzone = ImTrunc(self.LogSliderDeadzone * scale_factor)
|
|
self.ImageRounding = ImTrunc(self.ImageRounding * scale_factor)
|
|
self.ImageBorderSize = ImTrunc(self.ImageBorderSize * scale_factor)
|
|
self.TabRounding = ImTrunc(self.TabRounding * scale_factor)
|
|
self.TabBorderSize = ImTrunc(self.TabBorderSize * scale_factor)
|
|
self.TabMinWidthBase = ImTrunc(self.TabMinWidthBase * scale_factor)
|
|
self.TabMinWidthShrink = ImTrunc(self.TabMinWidthShrink * scale_factor)
|
|
self.TabCloseButtonMinWidthSelected = (self.TabCloseButtonMinWidthSelected > 0.0 and self.TabCloseButtonMinWidthSelected ~= FLT_MAX) and ImTrunc(self.TabCloseButtonMinWidthSelected * scale_factor) or self.TabCloseButtonMinWidthSelected
|
|
self.TabCloseButtonMinWidthUnselected = (self.TabCloseButtonMinWidthUnselected > 0.0 and self.TabCloseButtonMinWidthUnselected ~= FLT_MAX) and ImTrunc(self.TabCloseButtonMinWidthUnselected * scale_factor) or self.TabCloseButtonMinWidthUnselected
|
|
self.TabBarBorderSize = ImTrunc(self.TabBarBorderSize * scale_factor)
|
|
self.TabBarOverlineSize = ImTrunc(self.TabBarOverlineSize * scale_factor)
|
|
self.TreeLinesSize = ImTrunc(self.TreeLinesSize * scale_factor)
|
|
self.TreeLinesRounding = ImTrunc(self.TreeLinesRounding * scale_factor)
|
|
self.MenuItemRounding = ImTrunc(self.MenuItemRounding * scale_factor)
|
|
self.SelectableRounding = ImTrunc(self.SelectableRounding * scale_factor)
|
|
self.DragDropTargetRounding = ImTrunc(self.DragDropTargetRounding * scale_factor)
|
|
self.DragDropTargetBorderSize = ImTrunc(self.DragDropTargetBorderSize * scale_factor)
|
|
self.DragDropTargetPadding = ImTrunc(self.DragDropTargetPadding * scale_factor)
|
|
self.ColorMarkerSize = ImTrunc(self.ColorMarkerSize * scale_factor)
|
|
self.InputTextCursorSize = ImTrunc(self.InputTextCursorSize * scale_factor)
|
|
self.SeparatorSize = ImTrunc(self.SeparatorSize * scale_factor)
|
|
self.SeparatorTextBorderSize = ImTrunc(self.SeparatorTextBorderSize * scale_factor)
|
|
self.SeparatorTextPadding = ImTrunc(self.SeparatorTextPadding * scale_factor)
|
|
self.DockingSeparatorSize = ImTrunc(self.DockingSeparatorSize * scale_factor)
|
|
self.DisplayWindowPadding = ImTrunc(self.DisplayWindowPadding * scale_factor)
|
|
self.DisplaySafeAreaPadding = ImTrunc(self.DisplaySafeAreaPadding * scale_factor)
|
|
self.MouseCursorScale = ImTrunc(self.MouseCursorScale * scale_factor)
|
|
end
|
|
|
|
-- Data for resizing from resize grip / corner
|
|
local ImGuiResizeGripDef = {
|
|
{CornerPosN = ImVec2(1, 1), InnerDir = ImVec2(-1, -1), AngleMin12 = 0, AngleMax12 = 3}, -- Lower-right
|
|
{CornerPosN = ImVec2(0, 1), InnerDir = ImVec2( 1, -1), AngleMin12 = 3, AngleMax12 = 6} -- Lower-left
|
|
}
|
|
|
|
-- Data for resizing from borders
|
|
-- InnerDir: Normal toward inside, SegmentN1/SegmentN2: End positions, normalized (0,0: upper left), OuterAngle: Angle toward outside
|
|
--- [1] Left, [2] Right, [3] Up, [4] Down
|
|
local ImGuiResizeBorderDef = {
|
|
{InnerDir = ImVec2( 1, 0), SegmentN1 = ImVec2( 0, 1), SegmentN2 = ImVec2( 0, 0), OuterAngle = IM_PI * 1.00}, -- Left
|
|
{InnerDir = ImVec2(-1, 0), SegmentN1 = ImVec2( 1, 0), SegmentN2 = ImVec2( 1, 1), OuterAngle = IM_PI * 0.00}, -- Right
|
|
{InnerDir = ImVec2( 0, 1), SegmentN1 = ImVec2( 0, 0), SegmentN2 = ImVec2( 1, 0), OuterAngle = IM_PI * 1.50}, -- Up
|
|
{InnerDir = ImVec2( 0, -1), SegmentN1 = ImVec2( 1, 1), SegmentN2 = ImVec2( 0, 1), OuterAngle = IM_PI * 0.50} -- Down
|
|
}
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param border_n int
|
|
--- @param perp_padding float
|
|
--- @param thickness float
|
|
--- @return ImRect
|
|
--- @nodiscard
|
|
local function GetResizeBorderRect(window, border_n, perp_padding, thickness)
|
|
local rect = window:Rect()
|
|
if thickness == 0.0 then
|
|
ImVec2_CopyV(rect.Max, rect.Max.x - 1, rect.Max.y - 1)
|
|
end
|
|
if border_n == ImGuiDir.Left then
|
|
return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding)
|
|
end
|
|
if border_n == ImGuiDir.Right then
|
|
return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding)
|
|
end
|
|
if border_n == ImGuiDir.Up then
|
|
return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness)
|
|
end
|
|
if border_n == ImGuiDir.Down then
|
|
return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness)
|
|
end
|
|
IM_ASSERT(false)
|
|
return ImRect()
|
|
end
|
|
|
|
-- Known size hash
|
|
-- It is ok to call ImHashData on a string with known length but the ### operator won't be supported.
|
|
--- @param data table|number
|
|
--- @param size int? # size = -1 to indicate that `data` is a single number
|
|
--- @param seed int?
|
|
--- @return int
|
|
function ImHashData(data, size, seed)
|
|
seed = seed or 0
|
|
|
|
local FNV_OFFSET_BASIS = 0x811C9DC5
|
|
local FNV_PRIME = 0x01000193
|
|
|
|
local hash = bit32.bxor(FNV_OFFSET_BASIS, seed)
|
|
|
|
if size == -1 then --- @cast data number
|
|
hash = bit32.bxor(hash, data)
|
|
hash = bit32.band(hash * FNV_PRIME, 0xFFFFFFFF)
|
|
else
|
|
size = size or #data
|
|
for i = 1, size do
|
|
hash = bit32.bxor(hash, data[i])
|
|
hash = bit32.band(hash * FNV_PRIME, 0xFFFFFFFF)
|
|
end
|
|
end
|
|
|
|
return hash
|
|
end
|
|
|
|
-- Zero-terminated string hash, with support for ### to reset back to seed value.
|
|
-- e.g. "label###id" outputs the same hash as "id" (and "label" is generally displayed by the UI functions)
|
|
-- Use FNV1a, as one ImGui FIXME suggested
|
|
--- @param str string
|
|
--- @param size int?
|
|
--- @param seed int?
|
|
--- @return int
|
|
-- [Luau] memo for whole-string hashes: labels are re-hashed with the same seed every frame.
|
|
-- ponytail: whole-memo reset at HASH_MEMO_MAX entries, LRU if ID churn makes it thrash
|
|
local hash_memo, hash_memo_n, HASH_MEMO_MAX = {}, 0, 16384
|
|
local ImHashStrRaw
|
|
|
|
function ImHashStr(str, size, seed)
|
|
if seed == nil then seed = 0 end
|
|
if size == nil or size == #str then
|
|
local m = hash_memo[seed]
|
|
if m == nil then m = {}; hash_memo[seed] = m end
|
|
local h = m[str]
|
|
if h == nil then
|
|
if hash_memo_n >= HASH_MEMO_MAX then hash_memo = { [seed] = m }; table.clear(m); hash_memo_n = 0 end
|
|
h = ImHashStrRaw(str, #str, seed)
|
|
m[str] = h
|
|
hash_memo_n = hash_memo_n + 1
|
|
end
|
|
return h
|
|
end
|
|
return ImHashStrRaw(str, size, seed)
|
|
end
|
|
|
|
@native
|
|
function ImHashStrRaw(str, size, seed)
|
|
|
|
if str == "" or size == 0 then
|
|
return seed -- need to match cpp code edge case behavior while using FNV ourselves
|
|
end
|
|
|
|
local FNV_OFFSET_BASIS = 0x811C9DC5
|
|
local FNV_PRIME = 0x01000193
|
|
|
|
local hash = bit32.bxor(FNV_OFFSET_BASIS, seed)
|
|
|
|
local i = 1
|
|
local c
|
|
while i <= size do
|
|
c = string.byte(str, i)
|
|
|
|
-- `###` to reset back to initial hash value
|
|
if c == 35 and string.byte(str, i + 1) == 35 and string.byte(str, i + 2) == 35 then
|
|
hash = bit32.bxor(FNV_OFFSET_BASIS, seed)
|
|
i = i + 2
|
|
else
|
|
hash = bit32.bxor(hash, c)
|
|
hash = bit32.band(hash * FNV_PRIME, 0xFFFFFFFF)
|
|
end
|
|
|
|
i = i + 1
|
|
end
|
|
|
|
return hash
|
|
end
|
|
|
|
do --[[CharFromUtf8]]
|
|
|
|
local lengths = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0}
|
|
local masks = {0x00, 0x7f, 0x1f, 0x0f, 0x07}
|
|
local mins = {0x400000, 0, 0x80, 0x800, 0x10000}
|
|
local shiftc = {0, 18, 12, 6, 0}
|
|
local shifte = {0, 6, 4, 2, 0}
|
|
|
|
local s = {0, 0, 0, 0}
|
|
|
|
-- Convert UTF-8 to 32-bit character, process single character input.
|
|
-- A nearly-branchless UTF-8 decoder, based on work of Christopher Wellons (https://github.com/skeeto/branchless-utf8).
|
|
-- We handle UTF-8 decoding error by skipping forward.
|
|
--- @param in_text ImString
|
|
--- @param pos int
|
|
--- @param in_text_end int
|
|
--- @return int wanted
|
|
--- @return unsigned_int out_char
|
|
function ImStd.ImTextCharFromUtf8(in_text, pos, in_text_end)
|
|
local len = lengths[bit32.rshift(ImStrByte(in_text, pos), 3) + 1]
|
|
local wanted = len > 0 and len or 1
|
|
|
|
-- IMPORTANT: if in_text_end == nil it assume we have enough space!
|
|
if in_text_end == nil then
|
|
in_text_end = pos + wanted -- Max length, nulls will be taken into account.
|
|
end
|
|
|
|
-- Copy at most 'len' bytes, stop copying at 0 or past in_text_end. Branch predictor does a good job here,
|
|
-- so it is fast even with excessive branching.
|
|
s[1] = (pos < in_text_end) and ImStrByte(in_text, pos) or 0
|
|
s[2] = (pos + 1 < in_text_end) and ImStrByte(in_text, pos + 1) or 0
|
|
s[3] = (pos + 2 < in_text_end) and ImStrByte(in_text, pos + 2) or 0
|
|
s[4] = (pos + 3 < in_text_end) and ImStrByte(in_text, pos + 3) or 0
|
|
|
|
-- Assume a four-byte character and load four bytes. Unused bits are shifted out.
|
|
local out_char
|
|
out_char = bit32.lshift(bit32.band(s[1], masks[len + 1]), 18)
|
|
out_char = bit32.bor(out_char, bit32.lshift(bit32.band(s[2], 0x3f), 12))
|
|
out_char = bit32.bor(out_char, bit32.lshift(bit32.band(s[3], 0x3f), 6))
|
|
out_char = bit32.bor(out_char, bit32.lshift(bit32.band(s[4], 0x3f), 0))
|
|
out_char = bit32.rshift(out_char, shiftc[len + 1])
|
|
|
|
-- Accumulate the various error conditions.
|
|
local e = 0
|
|
e = bit32.lshift((out_char < mins[len + 1]) and 1 or 0, 6) -- non-canonical encoding
|
|
e = bit32.bor(e, bit32.lshift((bit32.rshift(out_char, 11) == 0x1b) and 1 or 0, 7)) -- surrogate half?
|
|
e = bit32.bor(e, bit32.lshift((out_char > IM_UNICODE_CODEPOINT_MAX) and 1 or 0, 8)) -- out of range we can store in ImWchar (FIXME: May evolve)
|
|
e = bit32.bor(e, bit32.rshift(bit32.band(s[2], 0xc0), 2))
|
|
e = bit32.bor(e, bit32.rshift(bit32.band(s[3], 0xc0), 4))
|
|
e = bit32.bor(e, bit32.rshift(s[4], 6))
|
|
e = bit32.bxor(e, 0x2a) -- top two bits of each tail byte correct?
|
|
e = bit32.rshift(e, shifte[len + 1])
|
|
|
|
if e ~= 0 then
|
|
-- No bytes are consumed when in_text[pos] == 0 || pos == in_text_end.
|
|
-- One byte is consumed in case of invalid first byte of in_text.
|
|
-- All available bytes (at most `len` bytes) are consumed on incomplete/invalid second to last bytes.
|
|
-- Invalid or incomplete input may consume less bytes than wanted, therefore every byte has to be inspected in s.
|
|
wanted = ImMin(wanted, (s[1] ~= 0 and 1 or 0) + (s[2] ~= 0 and 1 or 0) + (s[3] ~= 0 and 1 or 0) + (s[4] ~= 0 and 1 or 0))
|
|
out_char = IM_UNICODE_CODEPOINT_INVALID
|
|
end
|
|
|
|
return wanted, out_char
|
|
end
|
|
|
|
end
|
|
|
|
-- Based on stb_to_utf8() from github.com/nothings/stb/
|
|
--- @param buf char[]
|
|
--- @param buf_size int
|
|
--- @param c unsigned_int
|
|
local function ImTextCharToUtf8_inline(buf, buf_size, c)
|
|
if c < 0x80 then
|
|
buf[1] = c
|
|
return 1
|
|
end
|
|
if c < 0x800 then
|
|
if buf_size < 2 then
|
|
return 0
|
|
end
|
|
buf[1] = (0xc0 + bit32.rshift(c, 6))
|
|
buf[2] = (0x80 + bit32.band(c, 0x3f))
|
|
return 2
|
|
end
|
|
if c < 0x10000 then
|
|
if buf_size < 3 then
|
|
return 0
|
|
end
|
|
buf[1] = (0xe0 + bit32.rshift(c, 12))
|
|
buf[2] = (0x80 + bit32.band(bit32.rshift(c, 6), 0x3f))
|
|
buf[3] = (0x80 + bit32.band(c, 0x3f))
|
|
return 3
|
|
end
|
|
if c <= 0x10FFFF then
|
|
if buf_size < 4 then
|
|
return 0
|
|
end
|
|
buf[1] = (0xf0 + bit32.rshift(c, 18))
|
|
buf[2] = (0x80 + bit32.band(bit32.rshift(c, 12), 0x3f))
|
|
buf[3] = (0x80 + bit32.band(bit32.rshift(c, 6), 0x3f))
|
|
buf[4] = (0x80 + bit32.band(c, 0x3f))
|
|
return 4
|
|
end
|
|
|
|
-- Invalid code point, the max unicode is 0x10FFFF
|
|
return 0
|
|
end
|
|
|
|
--- @param out_buf [char, char, char, char, char]
|
|
--- @param c unsigned_int
|
|
function ImStd.ImTextCharToUtf8(out_buf, c)
|
|
local count = ImTextCharToUtf8_inline(out_buf, 5, c)
|
|
out_buf[count + 1] = 0
|
|
return count
|
|
end
|
|
|
|
-- Not optimal but we very rarely use this function.
|
|
--- @param in_text ImString
|
|
--- @param pos int
|
|
--- @param in_text_end int
|
|
--- @return int
|
|
function ImStd.ImTextCountUtf8BytesFromChar(in_text, pos, in_text_end)
|
|
local bytes, unused = ImStd.ImTextCharFromUtf8(in_text, pos, in_text_end)
|
|
return bytes
|
|
end
|
|
|
|
--- @param text ImString
|
|
--- @param in_text_start int
|
|
--- @param in_p int
|
|
function ImStd.ImTextFindPreviousUtf8Codepoint(text, in_text_start, in_p)
|
|
while in_p > in_text_start do
|
|
in_p = in_p - 1
|
|
if bit32.band(ImStrByte(text, in_p), 0xC0) ~= 0x80 then
|
|
return in_p
|
|
end
|
|
end
|
|
return in_text_start
|
|
end
|
|
|
|
--- @param text ImString
|
|
--- @param in_text_start int
|
|
--- @param in_text_end int
|
|
--- @param in_p int
|
|
--- @return int
|
|
function ImStd.ImTextFindValidUtf8CodepointEnd(text, in_text_start, in_text_end, in_p)
|
|
if in_text_start == in_p then
|
|
return in_text_start
|
|
end
|
|
|
|
local prev = ImStd.ImTextFindPreviousUtf8Codepoint(text, in_text_start, in_p)
|
|
local prev_c_len, prev_c = ImStd.ImTextCharFromUtf8(text, prev, in_text_end)
|
|
|
|
-- Check if the previous character is valid and fits within the range
|
|
if prev_c ~= IM_UNICODE_CODEPOINT_INVALID and prev_c_len <= (in_p - prev) then
|
|
return in_p
|
|
end
|
|
return prev
|
|
end
|
|
|
|
--- @param a ImVec2
|
|
--- @param b ImVec2
|
|
--- @param p ImVec2
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
function ImStd.ImLineClosestPoint(a, b, p)
|
|
local ap = p - a
|
|
local ab_dir = b - a
|
|
local dot = ap.x * ab_dir.x + ap.y * ab_dir.y
|
|
if dot < 0.0 then
|
|
return a
|
|
end
|
|
local ab_len_sqr = ab_dir.x * ab_dir.x + ab_dir.y * ab_dir.y
|
|
if dot > ab_len_sqr then
|
|
return b
|
|
end
|
|
return a + ab_dir * (dot / ab_len_sqr)
|
|
end
|
|
|
|
--- @param a ImVec2
|
|
--- @param b ImVec2
|
|
--- @param c ImVec2
|
|
--- @param p ImVec2
|
|
function ImStd.ImTriangleContainsPoint(a, b, c, p)
|
|
local b1 = ((p.x - b.x) * (a.y - b.y) - (p.y - b.y) * (a.x - b.x)) < 0.0
|
|
local b2 = ((p.x - c.x) * (b.y - c.y) - (p.y - c.y) * (b.x - c.x)) < 0.0
|
|
local b3 = ((p.x - a.x) * (c.y - a.y) - (p.y - a.y) * (c.x - a.x)) < 0.0
|
|
return (b1 == b2) and (b2 == b3)
|
|
end
|
|
|
|
--- @param a ImVec2
|
|
--- @param b ImVec2
|
|
--- @param c ImVec2
|
|
--- @param p ImVec2
|
|
--- @return float, float, float
|
|
function ImStd.ImTriangleBarycentricCoords(a, b, c, p)
|
|
local v0 = b - a
|
|
local v1 = c - a
|
|
local v2 = p - a
|
|
|
|
local denom = v0.x * v1.y - v1.x * v0.y
|
|
local out_v = (v2.x * v1.y - v1.x * v2.y) / denom
|
|
local out_w = (v0.x * v2.y - v2.x * v0.y) / denom
|
|
local out_u = 1.0 - out_v - out_w
|
|
|
|
return out_u, out_v, out_w
|
|
end
|
|
|
|
--- @param a ImVec2
|
|
--- @param b ImVec2
|
|
--- @param c ImVec2
|
|
--- @param p ImVec2
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
function ImStd.ImTriangleClosestPoint(a, b, c, p)
|
|
local proj_ab = ImStd.ImLineClosestPoint(a, b, p)
|
|
local proj_bc = ImStd.ImLineClosestPoint(b, c, p)
|
|
local proj_ca = ImStd.ImLineClosestPoint(c, a, p)
|
|
|
|
local dist2_ab = ImLengthSqr(p - proj_ab)
|
|
local dist2_bc = ImLengthSqr(p - proj_bc)
|
|
local dist2_ca = ImLengthSqr(p - proj_ca)
|
|
|
|
local m = ImMin(dist2_ab, ImMin(dist2_bc, dist2_ca))
|
|
|
|
if m == dist2_ab then
|
|
return proj_ab
|
|
elseif m == dist2_bc then
|
|
return proj_bc
|
|
else
|
|
return proj_ca
|
|
end
|
|
end
|
|
|
|
--- @param dst char[]
|
|
--- @param dst_pos int
|
|
--- @param src char[]
|
|
--- @param src_pos int
|
|
--- @param count size_t
|
|
function ImStd.ImStrncpy(dst, dst_pos, src, src_pos, count)
|
|
if count < 1 then
|
|
return
|
|
end
|
|
if count > 1 then
|
|
ImStd.memmove(dst, dst_pos, src, src_pos, count - 1)
|
|
end
|
|
dst[dst_pos + (count - 1)] = 0
|
|
end
|
|
|
|
-- find beginning-of-line
|
|
--- @param str char[]
|
|
--- @param mid_line int
|
|
--- @param begin int
|
|
function ImStd.ImStrbol(str, mid_line, begin)
|
|
IM_ASSERT_PARANOID(mid_line >= begin and mid_line <= #str)
|
|
while mid_line > begin and str[mid_line - 1] ~= 10 do
|
|
mid_line = mid_line - 1
|
|
end
|
|
return mid_line
|
|
end
|
|
|
|
-- Trim str by offsetting contents when there's leading data + writing a \0 at the trailing position. We use this in situation where the cost is negligible.
|
|
--- @param buf char[]
|
|
function ImStd.ImStrTrimBlanks(buf)
|
|
local p = 1 -- buf_begin
|
|
while buf[p] == 32 or buf[p] == 9 do -- Leading blanks
|
|
p = p + 1
|
|
end
|
|
local p_start = p
|
|
while buf[p] ~= 0 do -- Find end of string
|
|
p = p + 1
|
|
end
|
|
while p > p_start and (buf[p - 1] == 32 or buf[p - 1] == 9) do -- Trailing blanks
|
|
p = p - 1
|
|
end
|
|
if p_start > 1 then -- Copy memory if we had leading blanks
|
|
ImStd.memmove(buf, 1, buf, p_start, p - p_start)
|
|
end
|
|
buf[p - p_start + 1] = 0 -- Zero terminate
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] FONTS, TEXTURES
|
|
-------------------------------------------------------------------------------
|
|
-- Most of the relevant font logic is in imgui_draw.cpp.
|
|
-- Those are high-level support functions.
|
|
-------------------------------------------------------------------------------
|
|
-- - UpdateTexturesNewFrame() [Internal]
|
|
-- - UpdateTexturesEndFrame() [Internal]
|
|
-- - UpdateFontsNewFrame() [Internal]
|
|
-- - UpdateFontsEndFrame() [Internal]
|
|
-- - GetDefaultFont() [Internal]
|
|
-- - RegisterFontAtlas() [Internal]
|
|
-- - UnregisterFontAtlas() [Internal]
|
|
-- - SetCurrentFont() [Internal]
|
|
-- - UpdateCurrentFontSize() [Internal]
|
|
-- - PushFont()
|
|
-- - PopFont()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.UpdateCurrentFontSize(restore_font_size_after_scaling)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
g.Style.FontSizeBase = g.FontSizeBase
|
|
|
|
-- Early out to avoid hidden window keeping bakes referenced and out of GC reach.
|
|
-- - However this leave a pretty subtle and damning error surface area if g.FontBaked was mismatching.
|
|
-- Probably needs to be reevaluated into e.g. setting g.FontBaked = nil to mark it as dirty.
|
|
-- - Note that 'PushFont(); Begin(); End(); PopFont()' from within any collapsed window is not compromised, because Begin() calls SetCurrentWindow()->...->UpdateCurrentSize()
|
|
-- if (window ~= nil and window.SkipItems) then
|
|
-- local table = g.CurrentTable
|
|
-- if (table == nil or (table.CurrentColumn ~= -1 and table.Columns[table.CurrentColumn].IsSkipItems == false)) then
|
|
-- return
|
|
-- end
|
|
-- end
|
|
|
|
-- Restoring is pretty much only used by PopFont()
|
|
local final_size = (restore_font_size_after_scaling > 0.0) and restore_font_size_after_scaling or 0.0
|
|
if final_size == 0.0 then
|
|
final_size = g.FontSizeBase
|
|
|
|
-- Global scale factors
|
|
final_size = final_size * g.Style.FontScaleMain -- Main global scale factor
|
|
final_size = final_size * g.Style.FontScaleDpi -- Per-monitor/viewport DPI scale factor (in docking branch: automatically updated when io.ConfigDpiScaleFonts is enabled).
|
|
|
|
-- Window scale (mostly obsolete now)
|
|
if window ~= nil then
|
|
final_size = final_size * window.FontWindowScale
|
|
end
|
|
end
|
|
|
|
-- Round font size
|
|
-- - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet.
|
|
-- - We may support it better later and remove this rounding.
|
|
final_size = ImGui.GetRoundedFontSize(final_size)
|
|
final_size = ImClamp(final_size, 4.0, IMGUI_FONT_SIZE_MAX)
|
|
if (g.Font ~= nil and bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.RendererHasTextures) ~= 0) then
|
|
g.Font.CurrentRasterizerDensity = g.FontRasterizerDensity
|
|
end
|
|
g.FontSize = final_size
|
|
g.FontBaked = (g.Font ~= nil and window ~= nil) and g.Font:GetFontBaked(final_size) or nil
|
|
g.FontBakedScale = (g.FontBaked ~= nil) and (g.FontSize / g.FontBaked.Size) or 0.0
|
|
g.DrawListSharedData.FontSize = g.FontSize
|
|
g.DrawListSharedData.FontScale = g.FontBakedScale
|
|
end
|
|
|
|
-- Use ImDrawList:_SetTexture(), making our shared g.FontStack[] authoritative against window-local ImDrawList.
|
|
-- - Whereas ImDrawList:PushTexture()/PopTexture() is not to be used across Begin() calls.
|
|
-- - Note that we don't propagate current texture id when e.g. Begin()-ing into a new window, we never really did...
|
|
-- - Some code paths never really fully worked with multiple atlas textures.
|
|
-- - The right-ish solution may be to remove _SetTexture() and make AddText/RenderText lazily call PushTexture()/PopTexture()
|
|
-- the same way AddImage() does, but then all other primitives would also need to? I don't think we should tackle this problem
|
|
-- because we have a concrete need and a test bed for multiple atlas textures.
|
|
-- FIXME-NEWATLAS-V2: perhaps we can now leverage ImFontAtlasUpdateDrawListsTextures() ?
|
|
function ImGui.SetCurrentFont(font, font_size_before_scaling, font_size_after_scaling)
|
|
local g = GImGui
|
|
|
|
g.Font = font
|
|
g.FontSizeBase = font_size_before_scaling
|
|
ImGui.UpdateCurrentFontSize(font_size_after_scaling)
|
|
|
|
if font ~= nil then
|
|
IM_ASSERT(font and font:IsLoaded()) -- Font Atlas not created. Did you call io.Fonts:GetTexDataAsRGBA32 / GetTexDataAsAlpha8 ?
|
|
local atlas = font.OwnerAtlas
|
|
g.DrawListSharedData.FontAtlas = atlas
|
|
g.DrawListSharedData.Font = font
|
|
ImFontAtlasUpdateDrawListsSharedData(atlas)
|
|
if (g.CurrentWindow ~= nil) then
|
|
g.CurrentWindow.DrawList:_SetTexture(atlas.TexRef)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If you want to scale an existing font size! Read comments in imgui.h!
|
|
function ImGui.PushFont(font, font_size_base)
|
|
local g = GImGui
|
|
|
|
if font == nil then -- Before 1.92 (June 2025), PushFont(nil) == PushFont(GetDefaultFont())
|
|
font = g.Font
|
|
end
|
|
|
|
IM_ASSERT(font ~= nil)
|
|
IM_ASSERT(font_size_base >= 0.0)
|
|
|
|
g.FontStack:push_back(ImFontStackData(font, g.FontSizeBase, g.FontSize))
|
|
|
|
if font_size_base == 0.0 then
|
|
font_size_base = g.FontSizeBase -- Keep current font size
|
|
end
|
|
|
|
ImGui.SetCurrentFont(font, font_size_base, 0.0)
|
|
end
|
|
|
|
function ImGui.PopFont()
|
|
local g = GImGui
|
|
|
|
if (g.FontStack.Size <= 0) then
|
|
IM_ASSERT_USER_ERROR(0, "Calling PopFont() too many times!")
|
|
|
|
return
|
|
end
|
|
|
|
local font_stack_data = g.FontStack:back()
|
|
ImGui.SetCurrentFont(font_stack_data.Font, font_stack_data.FontSizeBeforeScaling, font_stack_data.FontSizeAfterScaling)
|
|
|
|
g.FontStack:pop_back()
|
|
end
|
|
|
|
function ImGui.UpdateTexturesNewFrame()
|
|
-- Cannot update every atlases based on atlas's FrameCount < g.FrameCount, because an atlas may be shared by multiple contexts with different frame count.
|
|
local g = GImGui
|
|
local has_textures = bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.RendererHasTextures) ~= 0
|
|
for _, atlas in g.FontAtlases:iter() do
|
|
if (atlas.OwnerContext == g) then
|
|
ImFontAtlasUpdateNewFrame(atlas, g.FrameCount, has_textures)
|
|
else
|
|
-- (1) If you manage font atlases yourself, e.g. create a ImFontAtlas yourself you need to call ImFontAtlasUpdateNewFrame() on it.
|
|
-- Otherwise, calling ImGui.CreateContext() without parameter will create an atlas owned by the context.
|
|
-- (2) If you have multiple font atlases, make sure the 'atlas.RendererHasTextures' as specified in the ImFontAtlasUpdateNewFrame() call matches for that.
|
|
-- (3) If you have multiple imgui contexts, they also need to have a matching value for ImGuiBackendFlags.RendererHasTextures.
|
|
IM_ASSERT(atlas.Builder ~= nil and atlas.Builder.FrameCount ~= -1)
|
|
IM_ASSERT(atlas.RendererHasTextures == has_textures)
|
|
end
|
|
end
|
|
|
|
for _, tex in g.UserTextures:iter() do
|
|
ImTextureDataUpdateNewFrame(tex)
|
|
end
|
|
end
|
|
|
|
-- Build a single texture list
|
|
function ImGui.UpdateTexturesEndFrame()
|
|
local g = GImGui
|
|
g.PlatformIO.Textures:resize(0)
|
|
for _, atlas in g.FontAtlases:iter() do
|
|
for _, tex in atlas.TexList:iter() do
|
|
-- We provide this information so backends can decide whether to destroy textures.
|
|
-- This means in practice that if N imgui contexts are created with a shared atlas, we assume all of them have a backend initialized.
|
|
tex.RefCount = atlas.RefCount
|
|
g.PlatformIO.Textures:push_back(tex)
|
|
end
|
|
end
|
|
for _, tex in g.UserTextures:iter() do
|
|
g.PlatformIO.Textures:push_back(tex)
|
|
end
|
|
end
|
|
|
|
function ImGui.UpdateFontsNewFrame()
|
|
local g = GImGui
|
|
if (bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.RendererHasTextures) == 0) then
|
|
for _, atlas in g.FontAtlases:iter() do
|
|
atlas.Locked = true
|
|
end
|
|
end
|
|
|
|
if (g.Style._NextFrameFontSizeBase ~= 0.0) then
|
|
g.Style.FontSizeBase = g.Style._NextFrameFontSizeBase
|
|
g.Style._NextFrameFontSizeBase = 0.0
|
|
end
|
|
|
|
-- Apply default font size the first time
|
|
local font = ImGui.GetDefaultFont()
|
|
if g.Style.FontSizeBase <= 0.0 then
|
|
g.Style.FontSizeBase = ((font.LegacySize > 0.0) and font.LegacySize or FONT_DEFAULT_SIZE_BASE)
|
|
end
|
|
|
|
-- Set initial font
|
|
g.Font = font
|
|
g.FontSizeBase = g.Style.FontSizeBase
|
|
g.FontSize = 0.0
|
|
local font_stack_data = ImFontStackData(font, g.Style.FontSizeBase, g.Style.FontSizeBase) -- <--- Will restore FontSize
|
|
ImGui.SetCurrentFont(font_stack_data.Font, font_stack_data.FontSizeBeforeScaling, 0.0) -- <--- but use 0.0f to enable scale
|
|
g.FontStack:push_back(font_stack_data)
|
|
IM_ASSERT(g.Font:IsLoaded())
|
|
end
|
|
|
|
function ImGui.UpdateFontsEndFrame()
|
|
ImGui.PopFont()
|
|
end
|
|
|
|
--- @return ImFont
|
|
function ImGui.GetDefaultFont()
|
|
local g = GImGui
|
|
local atlas = g.IO.Fonts
|
|
if (atlas.Builder == nil or atlas.Fonts.Size == 0) then
|
|
ImFontAtlasBuildMain(atlas)
|
|
end
|
|
return g.IO.FontDefault and g.IO.FontDefault or atlas.Fonts[1]
|
|
end
|
|
|
|
--- @param atlas ImFontAtlas
|
|
function ImGui.RegisterFontAtlas(atlas)
|
|
local g = GImGui
|
|
if (g.FontAtlases.Size == 0) then
|
|
IM_ASSERT(atlas == g.IO.Fonts)
|
|
end
|
|
atlas.RefCount = atlas.RefCount + 1
|
|
g.FontAtlases:push_back(atlas)
|
|
ImFontAtlasAddDrawListSharedData(atlas, g.DrawListSharedData)
|
|
for _, tex in atlas.TexList:iter() do
|
|
tex.RefCount = atlas.RefCount
|
|
end
|
|
end
|
|
|
|
--- @param atlas ImFontAtlas
|
|
function ImGui.UnregisterFontAtlas(atlas)
|
|
local g = GImGui
|
|
IM_ASSERT(atlas.RefCount > 0)
|
|
ImFontAtlasRemoveDrawListSharedData(atlas, g.DrawListSharedData)
|
|
g.FontAtlases:find_erase(atlas)
|
|
atlas.RefCount = atlas.RefCount - 1
|
|
for _, tex in atlas.TexList:iter() do
|
|
tex.RefCount = atlas.RefCount
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] INITIALIZATION, SHUTDOWN
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Internal state access - if you want to share Dear ImGui state between modules (e.g. DLL) or allocate it yourself
|
|
function ImGui.GetCurrentContext()
|
|
return GImGui
|
|
end
|
|
|
|
--- @param ctx ImGuiContext?
|
|
function ImGui.SetCurrentContext(ctx)
|
|
GImGui = ctx
|
|
|
|
ImGui._SetCurrentContext_Internal(ctx)
|
|
ImGui._SetCurrentContext_Widgets(ctx)
|
|
ImGui._SetCurrentContext_Tables(ctx)
|
|
end
|
|
|
|
--- @param alloc_func ImGuiMemAllocFunc
|
|
--- @param free_func ImGuiMemFreeFunc
|
|
--- @param user_data any
|
|
function ImGui.SetAllocatorFunctions(alloc_func, free_func, user_data)
|
|
GImAllocatorAllocFunc = alloc_func
|
|
GImAllocatorFreeFunc = free_func
|
|
GImAllocatorUserData = user_data
|
|
end
|
|
|
|
--- @param key ImGuiLocKey
|
|
--- @param text string
|
|
--- @return ImGuiLocEntry
|
|
--- @nodiscard
|
|
--- @package
|
|
local function ImGuiLocEntry(key, text)
|
|
return { Key = key, Text = text }
|
|
end
|
|
|
|
-- IMPORTANT: interactive elements requires a fixed ###xxx suffix, it must be same in ALL languages to allow for automation.
|
|
local GLocalizationEntriesEnUS = {
|
|
ImGuiLocEntry(ImGuiLocKey.VersionStr, "Dear ImGui WIP"),
|
|
ImGuiLocEntry(ImGuiLocKey.TableSizeOne, "Size column to fit###SizeOne"),
|
|
ImGuiLocEntry(ImGuiLocKey.TableSizeAllFit, "Size all columns to fit###SizeAll"),
|
|
ImGuiLocEntry(ImGuiLocKey.TableSizeAllDefault, "Size all columns to default###SizeAll"),
|
|
ImGuiLocEntry(ImGuiLocKey.TableReset, "Reset"),
|
|
--ImGuiLocEntry(ImGuiLocKey.TableResetAll, "Reset to default###ResetAll"),
|
|
ImGuiLocEntry(ImGuiLocKey.TableResetOrder, "Reset order###ResetOrder"),
|
|
ImGuiLocEntry(ImGuiLocKey.TableResetVisibility, "Reset visibility###ResetVisibility"),
|
|
ImGuiLocEntry(ImGuiLocKey.WindowingMainMenuBar, "(Main menu bar)"),
|
|
ImGuiLocEntry(ImGuiLocKey.WindowingPopup, "(Popup)"),
|
|
ImGuiLocEntry(ImGuiLocKey.WindowingUntitled, "(Untitled)"),
|
|
ImGuiLocEntry(ImGuiLocKey.OpenLink_s, "Open '%s'"),
|
|
ImGuiLocEntry(ImGuiLocKey.CopyLink, "Copy Link###CopyLink"),
|
|
ImGuiLocEntry(ImGuiLocKey.DockingHideTabBar, "Hide tab bar###HideTabBar"),
|
|
ImGuiLocEntry(ImGuiLocKey.DockingHoldShiftToDock, "Hold SHIFT to enable Docking window."),
|
|
ImGuiLocEntry(ImGuiLocKey.DockingDragToUndockOrMoveNode, "Click and drag to move or undock whole node."),
|
|
}
|
|
|
|
function ImGui.Initialize()
|
|
local g = GImGui
|
|
IM_ASSERT(not g.Initialized and not g.SettingsLoaded)
|
|
|
|
-- Add .ini handle for ImGuiWindow and ImGuiTable types
|
|
ImGui.WindowSettingsAddSettingsHandler()
|
|
if ImGui.TableSettingsAddSettingsHandler then ImGui.TableSettingsAddSettingsHandler() end
|
|
-- Initialize Docking
|
|
ImGui.DockContextInitialize(g)
|
|
-- Setup default localization table
|
|
ImGui.LocalizeRegisterEntries(GLocalizationEntriesEnUS, #GLocalizationEntriesEnUS)
|
|
|
|
-- Create default viewport
|
|
local viewport = ImGuiViewportP()
|
|
viewport.ID = IMGUI_VIEWPORT_DEFAULT_ID
|
|
viewport.Idx = 1
|
|
viewport.PlatformWindowCreated = true
|
|
viewport.Flags = ImGuiViewportFlags.OwnedByApp
|
|
g.Viewports:push_back(viewport)
|
|
g.ViewportCreatedCount = g.ViewportCreatedCount + 1
|
|
g.PlatformIO.Viewports:push_back(g.Viewports.Data[1])
|
|
|
|
-- Build KeysMayBeCharInput[] lookup table (1 bit per named key)
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
if (key >= ImGuiKey.K0 and key <= ImGuiKey.K9) or (key >= ImGuiKey.A and key <= ImGuiKey.Z) or (key >= ImGuiKey.Keypad0 and key <= ImGuiKey.Keypad9)
|
|
or key == ImGuiKey.Tab or key == ImGuiKey.Space or key == ImGuiKey.Apostrophe or key == ImGuiKey.Comma or key == ImGuiKey.Minus or key == ImGuiKey.Period
|
|
or key == ImGuiKey.Slash or key == ImGuiKey.Semicolon or key == ImGuiKey.Equal or key == ImGuiKey.LeftBracket or key == ImGuiKey.RightBracket or key == ImGuiKey.GraveAccent
|
|
or key == ImGuiKey.KeypadDecimal or key == ImGuiKey.KeypadDivide or key == ImGuiKey.KeypadMultiply or key == ImGuiKey.KeypadSubtract or key == ImGuiKey.KeypadAdd or key == ImGuiKey.KeypadEqual then
|
|
g.KeysMayBeCharInput:SetBit(key + 1)
|
|
end
|
|
end
|
|
|
|
-- ImDrawList/ImFontAtlas are designed to function without ImGui, and 99% of it works without an ImGui context.
|
|
-- But this link allows us to facilitate/handle a few edge cases better.
|
|
local atlas = g.IO.Fonts
|
|
g.DrawListSharedData.Context = g
|
|
ImGui.RegisterFontAtlas(atlas)
|
|
|
|
g.Initialized = true
|
|
end
|
|
|
|
--- @param shared_font_atlas? ImFontAtlas
|
|
function ImGui.CreateContext(shared_font_atlas)
|
|
local prev_ctx = ImGui.GetCurrentContext()
|
|
local ctx = ImGuiContext(shared_font_atlas)
|
|
ImGui.SetCurrentContext(ctx)
|
|
ImGui.Initialize()
|
|
if prev_ctx ~= nil then
|
|
ImGui.SetCurrentContext(prev_ctx) -- Restore previous context if any, else keep new one
|
|
end
|
|
return ctx
|
|
end
|
|
|
|
--- @param ctx? ImGuiContext
|
|
function ImGui.DestroyContext(ctx)
|
|
local prev_ctx = ImGui.GetCurrentContext()
|
|
if ctx == nil then
|
|
ctx = prev_ctx
|
|
end
|
|
ImGui.SetCurrentContext(ctx)
|
|
ImGui.Shutdown()
|
|
ImGui.SetCurrentContext((prev_ctx ~= ctx) and prev_ctx or nil)
|
|
end
|
|
|
|
-- This is faster if you are holding on a Window already as we don't need to perform a search.
|
|
--- @param window ImGuiWindow
|
|
--- @return any
|
|
function ImGui.FindWindowSettingsByWindow(window)
|
|
local g = GImGui
|
|
if window.SettingsOffset ~= -1 then
|
|
return g.SettingsWindows[window.SettingsOffset + 1]
|
|
end
|
|
return ImGui.FindWindowSettingsByID(window.ID)
|
|
end
|
|
|
|
-- We don't provide a FindWindowSettingsByName() because Docking system doesn't always hold on names.
|
|
-- This is called once per window .ini entry + once per newly instantiated window.
|
|
--- @param id ImGuiID
|
|
--- @return any
|
|
function ImGui.FindWindowSettingsByID(id)
|
|
local g = GImGui
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
if settings.ID == id and not settings.WantDelete then
|
|
return settings
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return ImGuiWindow?
|
|
function ImGui.FindFrontMostVisibleChildWindow(window)
|
|
for n = window.DC.ChildWindows.Size, 1, -1 do
|
|
if ImGui.IsWindowActiveAndVisible(window.DC.ChildWindows.Data[n]) then
|
|
return ImGui.FindFrontMostVisibleChildWindow(window.DC.ChildWindows.Data[n])
|
|
end
|
|
end
|
|
return window
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param cond ImGuiCond
|
|
--- @param allow bool
|
|
local function SetWindowConditionAllowFlags(window, cond, allow)
|
|
if allow then
|
|
window.SetWindowPosAllowFlags = bit32.bor(window.SetWindowPosAllowFlags, cond)
|
|
window.SetWindowSizeAllowFlags = bit32.bor(window.SetWindowSizeAllowFlags, cond)
|
|
window.SetWindowCollapsedAllowFlags = bit32.bor(window.SetWindowCollapsedAllowFlags, cond)
|
|
else
|
|
window.SetWindowPosAllowFlags = bit32.band(window.SetWindowPosAllowFlags, bit32.bnot(cond))
|
|
window.SetWindowSizeAllowFlags = bit32.band(window.SetWindowSizeAllowFlags, bit32.bnot(cond))
|
|
window.SetWindowCollapsedAllowFlags = bit32.band(window.SetWindowCollapsedAllowFlags, bit32.bnot(cond))
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param settings ImGuiWindowSettings
|
|
local function ApplyWindowSettings(window, settings)
|
|
if settings ~= nil then
|
|
local main_viewport = ImGui.GetMainViewport()
|
|
ImVec2_Copy(window.ViewportPos, main_viewport.Pos)
|
|
if (settings.ViewportId ~= 0) then
|
|
window.ViewportId = settings.ViewportId
|
|
ImVec2_Copy(window.ViewportPos, settings.ViewportPos)
|
|
end
|
|
window.Pos = ImTrunc(ImVec2(settings.Pos.x + window.ViewportPos.x, settings.Pos.y + window.ViewportPos.y))
|
|
if settings.Size.x > 0 and settings.Size.y > 0 then
|
|
local size = ImVec2(ImTrunc(settings.Size.x), ImTrunc(settings.Size.y))
|
|
ImVec2_Copy(window.Size, size)
|
|
ImVec2_Copy(window.SizeFull, size)
|
|
window.AutoFitFramesX = 0; window.AutoFitFramesY = 0
|
|
end
|
|
window.Collapsed = settings.Collapsed
|
|
window.DockId = settings.DockId or 0
|
|
window.DockOrder = settings.DockOrder or -1
|
|
SetWindowConditionAllowFlags(window, ImGuiCond.FirstUseEver, false)
|
|
end
|
|
|
|
-- So first call to CalcWindowContentSizes() doesn't return crazy values
|
|
ImVec2_Copy(window.DC.CursorStartPos, window.Pos)
|
|
ImVec2_Copy(window.DC.CursorMaxPos, window.Pos)
|
|
ImVec2_Copy(window.DC.IdealMaxPos, window.Pos)
|
|
if (bit32.band(window.Flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0) then
|
|
window.AutoFitFramesX = 2; window.AutoFitFramesY = 2
|
|
window.AutoFitOnlyGrows = false
|
|
else
|
|
window.AutoFitFramesX = (window.Size.x <= 0.0) and 2 or 0
|
|
window.AutoFitFramesY = (window.Size.y <= 0.0) and 2 or 0
|
|
window.AutoFitOnlyGrows = (window.AutoFitFramesX > 0) or (window.AutoFitFramesY > 0)
|
|
end
|
|
end
|
|
|
|
-- Note that 'settings' may be nil.
|
|
-- Sets _Once, _Appearing, _FirstUseEver. _FirstUseEver will be cleared again if there are settings.
|
|
--- @param window ImGuiWindow
|
|
--- @param settings ImGuiWindowSettings
|
|
local function InitOrLoadWindowSettings(window, settings)
|
|
-- Initial window state with e.g. default/arbitrary window position
|
|
-- Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window.
|
|
local main_viewport = ImGui.GetMainViewport()
|
|
ImVec2_Copy(window.Pos, main_viewport.Pos + ImVec2(60, 60))
|
|
ImVec2_Copy(window.Size, ImVec2(0, 0))
|
|
ImVec2_Copy(window.SizeFull, ImVec2(0, 0))
|
|
ImVec2_Copy(window.ViewportPos, main_viewport.Pos)
|
|
window.SetWindowPosAllowFlags = bit32.bor(ImGuiCond.Always, ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing)
|
|
window.SetWindowSizeAllowFlags = window.SetWindowPosAllowFlags
|
|
window.SetWindowCollapsedAllowFlags = window.SetWindowPosAllowFlags
|
|
window.SetWindowDockAllowFlags = window.SetWindowPosAllowFlags
|
|
ApplyWindowSettings(window, settings)
|
|
if settings ~= nil then
|
|
-- TODO: settings.LastUsedDate = g.SessionDate
|
|
end
|
|
end
|
|
|
|
--- @param name string
|
|
--- @param flags ImGuiWindowFlags
|
|
--- @return ImGuiWindow
|
|
local function CreateNewWindow(name, flags)
|
|
-- Create window the first time
|
|
--IMGUI_DEBUG_LOG("CreateNewWindow '%s', flags = 0x%08X\n", name, flags)
|
|
local g = GImGui
|
|
|
|
local window_id = ImHashStr(name)
|
|
|
|
local window = ImGuiWindow(g, name)
|
|
|
|
window.ID = window_id
|
|
window.Flags = flags
|
|
|
|
g.WindowsById[window_id] = window
|
|
|
|
local settings = nil
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoSavedSettings) == 0 then
|
|
settings = ImGui.FindWindowSettingsByWindow(window)
|
|
if settings ~= nil then
|
|
window.SettingsOffset = g.SettingsWindows:index_from_ptr(settings)
|
|
end
|
|
end
|
|
|
|
InitOrLoadWindowSettings(window, settings)
|
|
|
|
if bit32.band(flags, ImGuiWindowFlags.NoBringToFrontOnFocus) ~= 0 then
|
|
g.Windows:push_front(window) -- Quite slow but rare and only once
|
|
else
|
|
g.Windows:push_back(window)
|
|
end
|
|
|
|
return window
|
|
end
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] SETTINGS
|
|
-- (in-memory .ini; Roblox has no disk)
|
|
-------------------------------------------------------------------------------
|
|
-- - UpdateSettings() [Internal]
|
|
-- - MarkIniSettingsDirty() [Internal]
|
|
-- - FindSettingsHandler() [Internal]
|
|
-- - ClearIniSettings() [Internal]
|
|
-- - LoadIniSettingsFromDisk()
|
|
-- - LoadIniSettingsFromMemory()
|
|
-- - SaveIniSettingsToDisk()
|
|
-- - SaveIniSettingsToMemory()
|
|
-------------------------------------------------------------------------------
|
|
-- - CreateNewWindowSettings() [Internal]
|
|
-- - FindWindowSettingsByID() [Internal]
|
|
-- - FindWindowSettingsByWindow() [Internal]
|
|
-- - ClearWindowSettings() [Internal]
|
|
-- - WindowSettingsHandler_***() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Called by NewFrame()
|
|
function ImGui.UpdateSettings()
|
|
-- Load settings on first frame (if not explicitly loaded manually before)
|
|
local g = GImGui
|
|
if not g.SettingsLoaded then
|
|
if g.IO.IniFilename then ImGui.LoadIniSettingsFromDisk(g.IO.IniFilename) end
|
|
g.SettingsLoaded = true
|
|
end
|
|
|
|
-- Save settings (with a delay after the last modification, so we don't spam disk too much)
|
|
if g.SettingsDirtyTimer > 0.0 then
|
|
g.SettingsDirtyTimer = g.SettingsDirtyTimer - g.IO.DeltaTime
|
|
if g.SettingsDirtyTimer <= 0.0 then
|
|
-- No disk: always let the user know they can call SaveIniSettingsToMemory()
|
|
g.IO.WantSaveIniSettings = true -- Let user know they can call SaveIniSettingsToMemory(). user will need to clear io.WantSaveIniSettings themselves.
|
|
g.SettingsDirtyTimer = 0.0
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param window? ImGuiWindow
|
|
function ImGui.MarkIniSettingsDirty(window)
|
|
local g = GImGui
|
|
if window ~= nil and bit32.band(window.Flags, ImGuiWindowFlags.NoSavedSettings) ~= 0 then return end
|
|
if g.SettingsDirtyTimer <= 0.0 then
|
|
g.SettingsDirtyTimer = g.IO.IniSavingRate
|
|
end
|
|
end
|
|
|
|
--- @param handler ImGuiSettingsHandler
|
|
function ImGui.AddSettingsHandler(handler)
|
|
local g = GImGui
|
|
IM_ASSERT(ImGui.FindSettingsHandler(handler.TypeName) == nil)
|
|
if handler.TypeHash == 0 or handler.TypeHash == nil then handler.TypeHash = ImHashStr(handler.TypeName) end
|
|
g.SettingsHandlers:push_back(handler)
|
|
end
|
|
|
|
function ImGui.RemoveSettingsHandler(type_name)
|
|
local g = GImGui
|
|
local handler = ImGui.FindSettingsHandler(type_name)
|
|
if handler then g.SettingsHandlers:find_erase(handler) end
|
|
end
|
|
|
|
function ImGui.FindSettingsHandler(type_name)
|
|
local g = GImGui
|
|
local type_hash = ImHashStr(type_name)
|
|
for _, handler in g.SettingsHandlers:iter() do
|
|
if handler.TypeHash == type_hash then return handler end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Clear all settings (windows, tables, docking etc.)
|
|
function ImGui.ClearIniSettings()
|
|
local g = GImGui
|
|
g.SettingsIniData = ""
|
|
for _, handler in g.SettingsHandlers:iter() do
|
|
if handler.ClearAllFn then handler.ClearAllFn(g, handler) end
|
|
end
|
|
end
|
|
|
|
function ImGui.CleanupIniSettings(args)
|
|
local g = GImGui
|
|
if args.DiscardAll then
|
|
for _, handler in g.SettingsHandlers:iter() do
|
|
if (args.TypeHashFilter or 0) == 0 or handler.TypeHash == args.TypeHashFilter then
|
|
if handler.ClearAllFn then handler.ClearAllFn(g, handler) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.LoadIniSettingsFromDisk(ini_filename)
|
|
local data = ImStd.ImFileLoadToMemory and ImStd.ImFileLoadToMemory(ini_filename, "rb")
|
|
if not data or #data == 0 then return end
|
|
local parts = {}
|
|
for i = 1, #data do parts[i] = string.char(data[i]) end
|
|
ImGui.LoadIniSettingsFromMemory(table.concat(parts))
|
|
end
|
|
|
|
-- Zero-tolerance, no error reporting, cheap .ini parsing
|
|
-- Set ini_size==0 (or nil) to use the whole string (Lua strings know their length).
|
|
function ImGui.LoadIniSettingsFromMemory(ini_data, ini_size)
|
|
local g = GImGui
|
|
IM_ASSERT(g.Initialized)
|
|
--IM_ASSERT(not g.WithinFrameScope, "Cannot be called between NewFrame() and EndFrame()")
|
|
--IM_ASSERT(g.SettingsLoaded == false and g.FrameCount == 0)
|
|
|
|
-- For user convenience, we allow passing a non zero-terminated string (hence the ini_size parameter).
|
|
if ini_size and ini_size > 0 then ini_data = ini_data:sub(1, ini_size) end
|
|
g.SettingsIniData = ini_data
|
|
|
|
-- Call pre-read handlers
|
|
-- Some types will clear their data (e.g. dock information) some types will allow merge/override (window)
|
|
for _, handler in g.SettingsHandlers:iter() do
|
|
if handler.ReadInitFn then handler.ReadInitFn(g, handler) end
|
|
end
|
|
|
|
local entry_data, entry_handler = nil, nil
|
|
for line in ini_data:gmatch("[^\r\n]+") do
|
|
local c = line:sub(1, 1)
|
|
if c == ";" then
|
|
-- comment
|
|
elseif c == "[" and line:sub(-1) == "]" then
|
|
-- Parse "[Type][Name]". Note that 'Name' can itself contains [] characters, which is acceptable with the current format and parsing code.
|
|
local type_name, name = line:match("^%[([^%]]*)%]%[(.*)%]$")
|
|
if type_name then
|
|
entry_handler = ImGui.FindSettingsHandler(type_name)
|
|
entry_data = entry_handler and entry_handler.ReadOpenFn(g, entry_handler, name) or nil
|
|
else
|
|
entry_handler, entry_data = nil, nil
|
|
end
|
|
elseif entry_handler ~= nil and entry_data ~= nil then
|
|
-- Let type handler parse the line
|
|
entry_handler.ReadLineFn(g, entry_handler, entry_data, line)
|
|
end
|
|
end
|
|
g.SettingsLoaded = true
|
|
|
|
-- Call post-read handlers
|
|
for _, handler in g.SettingsHandlers:iter() do
|
|
if handler.ApplyAllFn then handler.ApplyAllFn(g, handler) end
|
|
end
|
|
end
|
|
|
|
function ImGui.SaveIniSettingsToDisk(ini_filename)
|
|
local g = GImGui
|
|
g.SettingsDirtyTimer = 0.0
|
|
-- Roblox: no disk. Call SaveIniSettingsToMemory() and persist the string yourself (e.g. DataStore).
|
|
end
|
|
|
|
-- Call registered handlers (e.g. SettingsHandlerWindow_WriteAll() + custom handlers) to write their stuff into a text buffer
|
|
--- @return string ini_data, int size
|
|
function ImGui.SaveIniSettingsToMemory()
|
|
local g = GImGui
|
|
g.SettingsDirtyTimer = 0.0
|
|
local buf = ImGuiTextBuffer()
|
|
for _, handler in g.SettingsHandlers:iter() do
|
|
handler.WriteAllFn(g, handler, buf)
|
|
end
|
|
g.SettingsIniData = buf:c_str()
|
|
return g.SettingsIniData, #g.SettingsIniData
|
|
end
|
|
|
|
function ImGui.CreateNewWindowSettings(name)
|
|
local g = GImGui
|
|
|
|
-- Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier.
|
|
if not g.IO.ConfigDebugIniSettings then
|
|
-- ImHashSkipUncontributingPrefix: skip everything up to the last "###"
|
|
local p = name:match("^.*()###")
|
|
if p then name = name:sub(p) end
|
|
end
|
|
local settings = ImGuiWindowSettings()
|
|
settings.ID = ImHashStr(name)
|
|
settings.Name = name
|
|
g.SettingsWindows:push_back(settings)
|
|
return settings
|
|
end
|
|
|
|
-- This will revert window to its initial state, including enabling the ImGuiCond.FirstUseEver/ImGuiCond.Once conditions once more.
|
|
function ImGui.ClearWindowSettings(name)
|
|
--IMGUI_DEBUG_LOG("ClearWindowSettings('%s')\n", name)
|
|
local window = ImGui.FindWindowByName(name)
|
|
if window ~= nil then
|
|
window.Flags = bit32.bor(window.Flags, ImGuiWindowFlags.NoSavedSettings)
|
|
InitOrLoadWindowSettings(window, nil)
|
|
if window.DockId and window.DockId ~= 0 and ImGui.DockContextProcessUndockWindow then
|
|
ImGui.DockContextProcessUndockWindow(GImGui, window, true)
|
|
end
|
|
end
|
|
local settings = window and ImGui.FindWindowSettingsByWindow(window) or ImGui.FindWindowSettingsByID(ImHashStr(name))
|
|
if settings then settings.WantDelete = true end
|
|
end
|
|
|
|
local function WindowSettingsHandler_ClearAll(ctx, handler)
|
|
local g = ctx
|
|
for _, window in g.Windows:iter() do window.SettingsOffset = -1 end
|
|
g.SettingsWindows:clear()
|
|
end
|
|
|
|
local function WindowSettingsHandler_ReadOpen(ctx, handler, name)
|
|
local id = ImHashStr(name)
|
|
local settings = ImGui.FindWindowSettingsByID(id)
|
|
if settings then
|
|
-- Clear existing if recycling previous entry
|
|
local fresh = ImGuiWindowSettings()
|
|
for k, v in pairs(fresh) do settings[k] = v end
|
|
settings.Name = name
|
|
else
|
|
settings = ImGui.CreateNewWindowSettings(name)
|
|
end
|
|
settings.ID = id
|
|
settings.WantApply = true
|
|
return settings
|
|
end
|
|
|
|
local function WindowSettingsHandler_ReadLine(ctx, handler, settings, line)
|
|
local x, y = line:match("^Pos=(%-?%d+),(%-?%d+)")
|
|
if x then ImVec2_CopyV(settings.Pos, tonumber(x), tonumber(y)) return end
|
|
x, y = line:match("^Size=(%-?%d+),(%-?%d+)")
|
|
if x then ImVec2_CopyV(settings.Size, tonumber(x), tonumber(y)) return end
|
|
x = line:match("^ViewportId=0x(%x+)")
|
|
if x then settings.ViewportId = tonumber(x, 16) return end
|
|
x, y = line:match("^ViewportPos=(%-?%d+),(%-?%d+)")
|
|
if x then ImVec2_CopyV(settings.ViewportPos, tonumber(x), tonumber(y)) return end
|
|
x = line:match("^Collapsed=(%d+)")
|
|
if x then settings.Collapsed = tonumber(x) ~= 0 return end
|
|
x = line:match("^IsChild=(%d+)")
|
|
if x then settings.IsChild = tonumber(x) ~= 0 return end
|
|
x = line:match("^LastUsed=(%d+)")
|
|
if x then settings.LastUsedDate = tonumber(x) return end
|
|
x, y = line:match("^DockId=0x(%x+),(%-?%d+)")
|
|
if x then settings.DockId = tonumber(x, 16); settings.DockOrder = tonumber(y) return end
|
|
x = line:match("^DockId=0x(%x+)")
|
|
if x then settings.DockId = tonumber(x, 16); settings.DockOrder = -1 return end
|
|
x = line:match("^ClassId=0x(%x+)")
|
|
if x then settings.ClassId = tonumber(x, 16) return end
|
|
end
|
|
|
|
-- Apply to existing windows (if any)
|
|
local function WindowSettingsHandler_ApplyAll(ctx, handler)
|
|
local g = ctx
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
if settings.WantApply then
|
|
local window = ImGui.FindWindowByID(settings.ID)
|
|
if window then ApplyWindowSettings(window, settings) end
|
|
settings.WantApply = false
|
|
end
|
|
end
|
|
end
|
|
|
|
local function WindowSettingsHandler_WriteAll(ctx, handler, buf)
|
|
-- Gather data from windows that were active during this session
|
|
-- (if a window wasn't opened in this session we preserve its settings)
|
|
local g = ctx
|
|
for _, window in g.Windows:iter() do
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoSavedSettings) == 0 then
|
|
local settings = ImGui.FindWindowSettingsByWindow(window)
|
|
if not settings then
|
|
settings = ImGui.CreateNewWindowSettings(window.Name)
|
|
window.SettingsOffset = g.SettingsWindows:index_from_ptr(settings)
|
|
end
|
|
IM_ASSERT(settings.ID == window.ID)
|
|
ImVec2_CopyV(settings.Pos, ImTrunc(window.Pos.x - window.ViewportPos.x), ImTrunc(window.Pos.y - window.ViewportPos.y))
|
|
ImVec2_CopyV(settings.Size, ImTrunc(window.SizeFull.x), ImTrunc(window.SizeFull.y))
|
|
settings.ViewportId = window.ViewportId or 0
|
|
ImVec2_CopyV(settings.ViewportPos, ImTrunc(window.ViewportPos.x), ImTrunc(window.ViewportPos.y))
|
|
settings.DockId = window.DockId or 0
|
|
settings.ClassId = window.WindowClass and window.WindowClass.ClassId or 0
|
|
settings.DockOrder = window.DockOrder or -1
|
|
settings.Collapsed = window.Collapsed
|
|
settings.IsChild = (window.RootWindow ~= window) -- Cannot rely on ImGuiWindowFlags.ChildWindow here as docked windows have this set.
|
|
settings.WantDelete = false
|
|
end
|
|
end
|
|
|
|
-- Write to text buffer
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
if not settings.WantDelete then
|
|
buf:appendf("[%s][%s]\n", handler.TypeName, settings.Name) -- [Window][name]
|
|
if settings.IsChild then
|
|
buf:appendf("IsChild=1\n")
|
|
buf:appendf("Size=%d,%d\n", settings.Size.x, settings.Size.y)
|
|
else
|
|
if settings.ViewportId ~= 0 and settings.ViewportId ~= IMGUI_VIEWPORT_DEFAULT_ID then
|
|
buf:appendf("ViewportPos=%d,%d\n", settings.ViewportPos.x, settings.ViewportPos.y)
|
|
buf:appendf("ViewportId=0x%08X\n", settings.ViewportId)
|
|
end
|
|
if settings.Pos.x ~= 0 or settings.Pos.y ~= 0 or settings.ViewportId == IMGUI_VIEWPORT_DEFAULT_ID then
|
|
buf:appendf("Pos=%d,%d\n", settings.Pos.x, settings.Pos.y)
|
|
end
|
|
if settings.Size.x ~= 0 or settings.Size.y ~= 0 then
|
|
buf:appendf("Size=%d,%d\n", settings.Size.x, settings.Size.y)
|
|
end
|
|
buf:appendf("Collapsed=%d\n", settings.Collapsed and 1 or 0)
|
|
if settings.DockId ~= 0 then
|
|
--buf:appendf("TabId=0x%08X\n", ImHashStr("#TAB", 4, settings.ID)) -- window.TabId: this is not read back but writing it makes "debugging" the .ini data easier.
|
|
if settings.DockOrder == -1 then
|
|
buf:appendf("DockId=0x%08X\n", settings.DockId)
|
|
else
|
|
buf:appendf("DockId=0x%08X,%d\n", settings.DockId, settings.DockOrder)
|
|
end
|
|
if settings.ClassId ~= 0 then buf:appendf("ClassId=0x%08X\n", settings.ClassId) end
|
|
end
|
|
end
|
|
buf:append("\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.WindowSettingsAddSettingsHandler()
|
|
local ini_handler = ImGuiSettingsHandler()
|
|
ini_handler.TypeName = "Window"
|
|
ini_handler.TypeHash = ImHashStr("Window")
|
|
ini_handler.ClearAllFn = WindowSettingsHandler_ClearAll
|
|
ini_handler.ReadOpenFn = WindowSettingsHandler_ReadOpen
|
|
ini_handler.ReadLineFn = WindowSettingsHandler_ReadLine
|
|
ini_handler.ApplyAllFn = WindowSettingsHandler_ApplyAll
|
|
ini_handler.WriteAllFn = WindowSettingsHandler_WriteAll
|
|
ImGui.AddSettingsHandler(ini_handler)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] LOGGING/CAPTURING
|
|
-------------------------------------------------------------------------------
|
|
-- All text output from the interface can be captured into tty/file/clipboard.
|
|
-- By default, tree nodes are automatically opened during logging.
|
|
-------------------------------------------------------------------------------
|
|
|
|
ImGuiLogFlags = ImGuiLogFlags or {
|
|
None = 0, OutputTTY = 1, OutputFile = 2, OutputBuffer = 4, OutputClipboard = 8, OutputMask_ = 15,
|
|
}
|
|
|
|
-- Pass text data straight to log (without being displayed)
|
|
local function LogTextV_Internal(g, fmt, ...)
|
|
local s = (select("#", ...) > 0) and string.format(fmt, ...) or fmt
|
|
if g.LogFile then
|
|
print(s) -- TTY (the only "file" available on Roblox)
|
|
else
|
|
g.LogBuffer:append(s)
|
|
end
|
|
end
|
|
|
|
function ImGui.LogText(fmt, ...)
|
|
local g = GImGui
|
|
if not g.LogEnabled then return end
|
|
LogTextV_Internal(g, fmt, ...)
|
|
end
|
|
ImGui.LogTextV = ImGui.LogText
|
|
|
|
-- Internal version that takes a position to decide on newline placement and pad items according to their depth.
|
|
-- We split text into individual lines to add current tree level padding
|
|
-- FIXME: This code is a little complicated perhaps, considering simplifying the whole system.
|
|
--- `text` is a Lua string, `text_begin/text_end` optional 1-based byte range (text_end exclusive).
|
|
function ImGui.LogRenderedText(ref_pos, text, text_end, text_begin)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
local prefix, suffix = g.LogNextPrefix, g.LogNextSuffix
|
|
g.LogNextPrefix = nil; g.LogNextSuffix = nil
|
|
|
|
text_begin = text_begin or 1
|
|
if not text_end then text_end = ImGui.FindRenderedTextEnd(text, text_end) end
|
|
|
|
local log_new_line = ref_pos and (ref_pos.y > g.LogLinePosY + ImMax(g.Style.FramePadding.y, g.Style.ItemSpacing.y) + 1)
|
|
if ref_pos then g.LogLinePosY = ref_pos.y end
|
|
if log_new_line then
|
|
ImGui.LogText("\n")
|
|
g.LogLineFirstItem = true
|
|
end
|
|
|
|
if prefix then ImGui.LogRenderedText(ref_pos, prefix, #prefix + 1) end -- Calculate end ourself to ensure "##" are included here.
|
|
|
|
-- Re-adjust padding if we have popped out of our starting depth
|
|
if g.LogDepthRef > window.DC.TreeDepth then g.LogDepthRef = window.DC.TreeDepth end
|
|
local tree_depth = window.DC.TreeDepth - g.LogDepthRef
|
|
|
|
local text_remaining = text_begin
|
|
while true do
|
|
-- Split the string. Each new line (after a '\n') is followed by indentation corresponding to the current depth of our log entry.
|
|
-- We don't add a trailing \n yet to allow a subsequent item on the same line to be captured.
|
|
local line_start = text_remaining
|
|
local nl = string.find(text, "\n", line_start, true)
|
|
local line_end = (nl and nl < text_end) and nl or text_end
|
|
local is_last_line = (line_end == text_end)
|
|
if line_start ~= line_end or not is_last_line then
|
|
local indentation = g.LogLineFirstItem and tree_depth * 4 or 1
|
|
ImGui.LogText(string.rep(" ", indentation) .. string.sub(text, line_start, line_end - 1))
|
|
g.LogLineFirstItem = false
|
|
if string.byte(text, line_end) == 10 then
|
|
ImGui.LogText("\n")
|
|
g.LogLineFirstItem = true
|
|
end
|
|
end
|
|
if is_last_line then break end
|
|
text_remaining = line_end + 1
|
|
end
|
|
|
|
if suffix then ImGui.LogRenderedText(ref_pos, suffix, #suffix + 1) end
|
|
end
|
|
|
|
-- Start logging/capturing text output
|
|
function ImGui.LogBegin(flags, auto_open_depth)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT(g.LogEnabled == false)
|
|
g.LogEnabled = true; g.ItemUnclipByLog = true
|
|
g.LogFlags = flags
|
|
g.LogWindow = window
|
|
g.LogNextPrefix = nil; g.LogNextSuffix = nil
|
|
g.LogDepthRef = window.DC.TreeDepth
|
|
g.LogDepthToExpand = ((auto_open_depth or -1) >= 0) and auto_open_depth or g.LogDepthToExpandDefault
|
|
g.LogLinePosY = FLT_MAX
|
|
g.LogLineFirstItem = true
|
|
g.LogBuffer = ImGuiTextBuffer()
|
|
end
|
|
|
|
-- Important: doesn't copy underlying data, use carefully (prefix/suffix must be in scope at the time of the next LogRenderedText)
|
|
function ImGui.LogSetNextTextDecoration(prefix, suffix)
|
|
local g = GImGui
|
|
g.LogNextPrefix = prefix
|
|
g.LogNextSuffix = suffix
|
|
end
|
|
|
|
function ImGui.LogToTTY(auto_open_depth)
|
|
local g = GImGui
|
|
if g.LogEnabled then return end
|
|
ImGui.LogBegin(ImGuiLogFlags.OutputTTY, auto_open_depth or -1)
|
|
g.LogFile = "stdout"
|
|
end
|
|
|
|
-- Start logging/capturing text output to given file
|
|
function ImGui.LogToFile(auto_open_depth, filename)
|
|
-- Roblox: no file access. Falls back to TTY (output window).
|
|
ImGui.LogToTTY(auto_open_depth)
|
|
end
|
|
|
|
-- Start logging/capturing text output to clipboard
|
|
function ImGui.LogToClipboard(auto_open_depth)
|
|
local g = GImGui
|
|
if g.LogEnabled then return end
|
|
ImGui.LogBegin(ImGuiLogFlags.OutputClipboard, auto_open_depth or -1)
|
|
end
|
|
|
|
function ImGui.LogToBuffer(auto_open_depth)
|
|
local g = GImGui
|
|
if g.LogEnabled then return end
|
|
ImGui.LogBegin(ImGuiLogFlags.OutputBuffer, auto_open_depth or -1)
|
|
end
|
|
|
|
function ImGui.LogFinish()
|
|
local g = GImGui
|
|
if not g.LogEnabled then return end
|
|
ImGui.LogText("\n")
|
|
local out = bit32.band(g.LogFlags, ImGuiLogFlags.OutputMask_)
|
|
if out == ImGuiLogFlags.OutputClipboard then
|
|
if not g.LogBuffer:empty() then ImGui.SetClipboardText(g.LogBuffer:c_str()) end
|
|
end
|
|
g.LogEnabled = false; g.ItemUnclipByLog = false
|
|
g.LogFlags = ImGuiLogFlags.None
|
|
g.LogFile = nil
|
|
g.LogBuffer = ImGuiTextBuffer()
|
|
end
|
|
|
|
-- Helper to display logging buttons
|
|
-- FIXME-OBSOLETE: We should probably obsolete this and let the user have their own helper (this is one of the oldest function alive!)
|
|
function ImGui.LogButtons()
|
|
local g = GImGui
|
|
ImGui.PushID("LogButtons")
|
|
local log_to_tty = ImGui.Button("Log To TTY"); ImGui.SameLine()
|
|
local log_to_file = ImGui.Button("Log To File"); ImGui.SameLine()
|
|
local log_to_clipboard = ImGui.Button("Log To Clipboard"); ImGui.SameLine()
|
|
ImGui.PushItemFlag(ImGuiItemFlags.NoTabStop, true)
|
|
ImGui.SetNextItemWidth(ImGui.CalcTextSize("999").x)
|
|
g.LogDepthToExpandDefault = ImGui.SliderInt("Default Depth", g.LogDepthToExpandDefault, 0, 9, nil)
|
|
ImGui.PopItemFlag()
|
|
ImGui.PopID()
|
|
|
|
-- Start logging at the end of the function so that the buttons don't appear in the log
|
|
if log_to_tty then ImGui.LogToTTY() end
|
|
if log_to_file then ImGui.LogToFile() end
|
|
if log_to_clipboard then ImGui.LogToClipboard() end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return ImGuiWindow?
|
|
local function GetWindowForTitleDisplay(window)
|
|
if window.DockNodeAsHost then
|
|
return window.DockNodeAsHost.VisibleWindow
|
|
else
|
|
return window
|
|
end
|
|
end
|
|
|
|
-- Code not using ItemAdd() may need to call this manually otherwise ActiveId will be cleared. In IMGUI_VERSION_NUM < 18717 this was called by GetID().
|
|
--- @param id ImGuiID
|
|
@native
|
|
function ImGui.KeepAliveID(id)
|
|
local g = GImGui
|
|
|
|
if g.ActiveId == id then
|
|
g.ActiveIdIsAlive = id
|
|
end
|
|
|
|
if g.DeactivatedItemData.ID == id then
|
|
g.DeactivatedItemData.IsAlive = true
|
|
end
|
|
end
|
|
|
|
-- Test if mouse cursor is hovering given rectangle
|
|
-- NB- Rectangle is clipped by our current clip setting
|
|
-- NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding)
|
|
--- @param r_min ImVec2
|
|
--- @param r_max ImVec2
|
|
--- @param clip? bool
|
|
@native
|
|
function ImGui.IsMouseHoveringRect(r_min, r_max, clip)
|
|
if clip == nil then clip = true end
|
|
|
|
local g = GImGui
|
|
-- [Luau] scalar version of: rect_clipped = ImRect(r_min, r_max); ClipWith(window.ClipRect); ContainsWithPad(); viewport Overlaps()
|
|
-- Clip
|
|
local x0, y0, x1, y1 = r_min.x, r_min.y, r_max.x, r_max.y
|
|
if clip then
|
|
local cr = g.CurrentWindow.ClipRect
|
|
local cmin, cmax = cr.Min, cr.Max
|
|
x0 = math.max(x0, cmin.x); y0 = math.max(y0, cmin.y)
|
|
x1 = math.min(x1, cmax.x); y1 = math.min(y1, cmax.y)
|
|
end
|
|
|
|
-- Hit testing, expanded for touch input
|
|
local p, pad = g.IO.MousePos, g.Style.TouchExtraPadding
|
|
if not (p.x >= x0 - pad.x and p.y >= y0 - pad.y and p.x < x1 + pad.x and p.y < y1 + pad.y) then
|
|
return false
|
|
end
|
|
local vp = g.MouseViewport
|
|
local vx0, vy0 = vp.Pos.x, vp.Pos.y
|
|
local vx1, vy1 = vx0 + vp.Size.x, vy0 + vp.Size.y
|
|
if not (vx0 <= x1 and vx1 >= x0 and vy0 <= y1 and vy1 >= y0) then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Return if a mouse click/drag went past the given threshold. Valid to call during the MouseReleased frame.
|
|
-- [Internal] This doesn't test if the button is pressed
|
|
--- @param button ImGuiMouseButton
|
|
--- @param lock_threshold? float
|
|
function ImGui.IsMouseDragPastThreshold(button, lock_threshold)
|
|
if lock_threshold == nil then lock_threshold = -1.0 end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3) -- IM_COUNTOF(g.IO.MouseDown)
|
|
if lock_threshold < 0.0 then
|
|
lock_threshold = g.IO.MouseDragThreshold
|
|
end
|
|
return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
--- @param lock_threshold? float
|
|
function ImGui.IsMouseDragging(button, lock_threshold)
|
|
if lock_threshold == nil then lock_threshold = -1.0 end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3) -- IM_COUNTOF(g.IO.MouseDown)
|
|
if not g.IO.MouseDown[button] then
|
|
return false
|
|
end
|
|
return ImGui.IsMouseDragPastThreshold(button, lock_threshold)
|
|
end
|
|
|
|
-- This is called TeleportMousePos() and not SetMousePos() to emphasis that setting MousePosPrev will effectively clear mouse delta as well.
|
|
-- It is expected you only call this if (io.BackendFlags & ImGuiBackendFlags.HasSetMousePos) is set and supported by backend.
|
|
--- @param pos ImVec2
|
|
function ImGui.TeleportMousePos(pos)
|
|
local g = GImGui
|
|
ImVec2_Copy(g.IO.MousePos, pos)
|
|
ImVec2_Copy(g.IO.MousePosPrev, pos)
|
|
ImVec2_CopyV(g.IO.MouseDelta, 0.0, 0.0)
|
|
g.IO.WantSetMousePos = true
|
|
-- IMGUI_DEBUG_LOG_IO("TeleportMousePos: (%.1f,%.1f)", io.MousePos.x, io.MousePos.y)
|
|
end
|
|
|
|
-- Called from within ItemAdd: at this point we can read from NextItemData and write to LastItemData
|
|
--- @param id ImGuiID
|
|
function ImGui.ItemHandleShortcut(id)
|
|
local g = GImGui
|
|
local flags = g.NextItemData.ShortcutFlags
|
|
IM_ASSERT(bit32.band(flags, bit32.bnot(ImGuiInputFlags.SupportedBySetNextItemShortcut)) == 0) -- Passing flags not supported by SetNextItemShortcut()!
|
|
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.Disabled) ~= 0 then
|
|
return
|
|
end
|
|
if bit32.band(flags, ImGuiInputFlags.Tooltip) ~= 0 then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HasShortcut)
|
|
g.LastItemData.Shortcut = g.NextItemData.Shortcut
|
|
end
|
|
if not ImGui.Shortcut(g.NextItemData.Shortcut, bit32.band(flags, ImGuiInputFlags.SupportedByShortcut), id) or g.NavActivateId ~= 0 then
|
|
return
|
|
end
|
|
|
|
-- FIXME: Generalize Activation queue?
|
|
g.NavActivateId = id -- Will effectively disable clipping.
|
|
g.NavActivateFlags = bit32.bor(ImGuiActivateFlags.PreferInput, ImGuiActivateFlags.FromShortcut)
|
|
-- if (g.ActiveId == 0 || g.ActiveId == id)
|
|
g.NavActivateDownId = id
|
|
g.NavActivatePressedId = id
|
|
ImGui.NavHighlightActivated(id)
|
|
end
|
|
|
|
-- Declare item bounding box for clipping and interaction.
|
|
-- Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface
|
|
-- declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction.
|
|
-- THIS IS IN THE PERFORMANCE CRITICAL PATH (UNTIL THE CLIPPING TEST AND EARLY-RETURN)
|
|
--- @param bb ImRect
|
|
--- @param id ImGuiID
|
|
--- @param nav_bb_arg? ImRect
|
|
--- @param extra_flags? ImGuiItemFlags
|
|
@native
|
|
function ImGui.ItemAdd(bb, id, nav_bb_arg, extra_flags)
|
|
if extra_flags == nil then extra_flags = 0 end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
-- Set item data
|
|
-- (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags.HasDisplayRect is set)
|
|
g.LastItemData.ID = id
|
|
ImRect_Copy(g.LastItemData.Rect, bb)
|
|
ImRect_Copy(g.LastItemData.NavRect, nav_bb_arg and nav_bb_arg or bb)
|
|
g.LastItemData.ItemFlags = bit32.bor(g.CurrentItemFlags, g.NextItemData.ItemFlagsSet, extra_flags)
|
|
g.LastItemData.StatusFlags = ImGuiItemStatusFlags.None
|
|
-- Note: we don't copy 'g.NextItemData.SelectionUserData' to an hypothetical g.LastItemData.SelectionUserData: since the former is not cleared.
|
|
|
|
if id ~= 0 then
|
|
ImGui.KeepAliveID(id)
|
|
|
|
-- Directional navigation processing
|
|
-- Runs prior to clipping early-out
|
|
-- (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget
|
|
-- (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests
|
|
-- unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of
|
|
-- thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame.
|
|
-- We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able
|
|
-- to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick).
|
|
-- We intentionally don't check if g.NavWindow ~= nil because g.NavAnyRequest should only be set when it is non nil.
|
|
-- If we crash on a nil g.NavWindow we need to fix the bug elsewhere.
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.NoNav) == 0 then
|
|
-- FIXME-NAV: investigate changing the window tests into a simple 'if (g.NavFocusScopeId == g.CurrentFocusScopeId)' test.
|
|
window.DC.NavLayersActiveMaskNext = bit32.bor(window.DC.NavLayersActiveMaskNext, bit32.lshift(1, window.DC.NavLayerCurrent))
|
|
|
|
if g.NavId == id or g.NavAnyRequest then
|
|
if g.NavWindow.RootWindowForNav == window.RootWindowForNav then
|
|
if window == g.NavWindow or bit32.band(bit32.bor(window.ChildFlags, g.NavWindow.ChildFlags), ImGuiChildFlags.NavFlattened) ~= 0 then
|
|
ImGui.NavProcessItem()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if bit32.band(g.NextItemData.HasFlags, ImGuiNextItemDataFlags.HasShortcut) ~= 0 then
|
|
ImGui.ItemHandleShortcut(id)
|
|
end
|
|
end
|
|
|
|
-- Lightweight clear of SetNextItemXXX data.
|
|
g.NextItemData.HasFlags = ImGuiNextItemDataFlags.None
|
|
g.NextItemData.ItemFlagsSet = ImGuiItemFlags.None
|
|
|
|
-- Clipping test
|
|
-- (this is an inline copy of IsClippedEx() so we can reuse the is_rect_visible value, otherwise we'd do 'if (IsClippedEx(bb, id)) return false')
|
|
-- g.NavActivateId is not necessarily == g.NavId, in the case of remote activation (e.g. shortcuts)
|
|
local bmin, bmax, cr = bb.Min, bb.Max, window.ClipRect
|
|
local is_rect_visible = bmin.x <= cr.Max.x and bmax.x >= cr.Min.x and bmin.y <= cr.Max.y and bmax.y >= cr.Min.y
|
|
if not is_rect_visible then
|
|
if id == 0 or not (id == g.ActiveId or id == g.ActiveIdPreviousFrame or id == g.NavId or id == g.NavActivateId or g.ItemUnclipByLog) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
if id ~= 0 and g.DeactivatedItemData.ID == id then
|
|
g.DeactivatedItemData.ElapseFrame = g.FrameCount
|
|
end
|
|
|
|
-- We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them)
|
|
if is_rect_visible then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Visible)
|
|
end
|
|
|
|
if ImGui.IsMouseHoveringRect(bb.Min, bb.Max) then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredRect)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] LAYOUT
|
|
-------------------------------------------------------------------------------
|
|
-- - ItemSize()
|
|
-- - SameLine()
|
|
-- - GetCursorScreenPos()
|
|
-- - SetCursorScreenPos()
|
|
-- - GetCursorPos(), GetCursorPosX(), GetCursorPosY()
|
|
-- - SetCursorPos(), SetCursorPosX(), SetCursorPosY()
|
|
-- - GetCursorStartPos()
|
|
-- - Indent()
|
|
-- - Unindent()
|
|
-- - SetNextItemWidth()
|
|
-- - PushItemWidth()
|
|
-- - PushMultiItemsWidths()
|
|
-- - PopItemWidth()
|
|
-- - CalcItemWidth()
|
|
-- - CalcItemSize()
|
|
-- - GetTextLineHeight()
|
|
-- - GetTextLineHeightWithSpacing()
|
|
-- - GetFrameHeight()
|
|
-- - GetFrameHeightWithSpacing()
|
|
-- - GetContentRegionMax()
|
|
-- - GetContentRegionAvail(),
|
|
-- - BeginGroup()
|
|
-- - EndGroup()
|
|
-- Also see in imgui_widgets: tab bars, and in imgui_tables: tables, columns.
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Advance cursor given item size for layout.
|
|
-- Register minimum needed size so it can extend the bounding box used for auto-fit calculation.
|
|
-- See comments in ItemAdd() about how/why the size provided to ItemSize() vs ItemAdd() may often different.
|
|
-- THIS IS IN THE PERFORMANCE CRITICAL PATH.
|
|
--- @param size_or_bb ImVec2|ImRect
|
|
--- @param text_baseline_y? float
|
|
@native
|
|
function ImGui.ItemSize(size_or_bb, text_baseline_y)
|
|
if text_baseline_y == nil then text_baseline_y = -1.0 end
|
|
|
|
local size_x, size_y
|
|
local bb_min = size_or_bb.Min
|
|
if bb_min then
|
|
--- @cast size_or_bb ImRect
|
|
size_x = size_or_bb.Max.x - bb_min.x; size_y = size_or_bb.Max.y - bb_min.y
|
|
else
|
|
--- @cast size_or_bb ImVec2
|
|
size_x = size_or_bb.x; size_y = size_or_bb.y
|
|
end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
if window.SkipItems then
|
|
return
|
|
end
|
|
local dc = window.DC
|
|
local mathMax = math.max
|
|
|
|
-- We increase the height in this function to accommodate for baseline offset.
|
|
-- In theory we should be offsetting the starting position (window.DC.CursorPos), that will be the topic of a larger refactor,
|
|
-- but since ItemSize() is not yet an API that moves the cursor (to handle e.g. wrapping) enlarging the height has the same effect.
|
|
local offset_to_match_baseline_y
|
|
if text_baseline_y >= 0 then
|
|
offset_to_match_baseline_y = mathMax(0, dc.CurrLineTextBaseOffset - text_baseline_y)
|
|
else
|
|
offset_to_match_baseline_y = 0
|
|
end
|
|
|
|
local cursor, prev_line, max_pos = dc.CursorPos, dc.CursorPosPrevLine, dc.CursorMaxPos
|
|
local line_y1
|
|
if dc.IsSameLine then
|
|
line_y1 = prev_line.y
|
|
else
|
|
line_y1 = cursor.y
|
|
end
|
|
|
|
local line_height = mathMax(dc.CurrLineSize.y, cursor.y - line_y1 + size_y + offset_to_match_baseline_y)
|
|
local spacing_y = g.Style.ItemSpacing.y
|
|
|
|
-- Always align ourselves on pixel boundaries
|
|
-- if (g.IO.KeyAlt) window.DrawList:AddRect(window.DC.CursorPos, window.DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)) -- [DEBUG]
|
|
prev_line.x = cursor.x + size_x
|
|
prev_line.y = line_y1
|
|
cursor.x = math.floor(window.Pos.x + dc.Indent.x + dc.ColumnsOffset.x) -- Next line
|
|
cursor.y = math.floor(line_y1 + line_height + spacing_y) -- Next line
|
|
max_pos.x = mathMax(max_pos.x, prev_line.x)
|
|
max_pos.y = mathMax(max_pos.y, cursor.y - spacing_y)
|
|
-- if (g.IO.KeyAlt) window.DrawList:AddCircle(window.DC.CursorMaxPos, 3.0, IM_COL32(255,0,0,255), 4) -- [DEBUG]
|
|
|
|
dc.PrevLineSize.y = line_height
|
|
dc.CurrLineSize.y = 0
|
|
dc.PrevLineTextBaseOffset = mathMax(dc.CurrLineTextBaseOffset, text_baseline_y)
|
|
dc.CurrLineTextBaseOffset = 0
|
|
dc.IsSetPos = false
|
|
dc.IsSameLine = false
|
|
|
|
-- Horizontal layout mode
|
|
if (dc.LayoutType == ImGuiLayoutType.Horizontal) then
|
|
ImGui.SameLine()
|
|
end
|
|
end
|
|
|
|
-- Gets back to previous line and continue with horizontal layout
|
|
-- offset_from_start_x == 0 : follow right after previous item
|
|
-- offset_from_start_x != 0 : align to specified x position (relative to window/group left)
|
|
-- spacing_w < 0 : use default spacing if offset_from_start_x == 0, no spacing if offset_from_start_x != 0
|
|
-- spacing_w >= 0 : enforce spacing amount
|
|
--- @param offset_from_start_x float?
|
|
--- @param spacing_w float?
|
|
@native
|
|
function ImGui.SameLine(offset_from_start_x, spacing_w)
|
|
if offset_from_start_x == nil then offset_from_start_x = 0.0 end
|
|
if spacing_w == nil then spacing_w = -1.0 end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
if window.SkipItems then
|
|
return
|
|
end
|
|
|
|
if offset_from_start_x ~= 0.0 then
|
|
if spacing_w < 0.0 then spacing_w = 0.0 end
|
|
window.DC.CursorPos.x = window.Pos.x - window.Scroll.x + offset_from_start_x + spacing_w + window.DC.GroupOffset.x + window.DC.ColumnsOffset.x
|
|
window.DC.CursorPos.y = window.DC.CursorPosPrevLine.y
|
|
else
|
|
if spacing_w < 0.0 then spacing_w = g.Style.ItemSpacing.x end
|
|
window.DC.CursorPos.x = window.DC.CursorPosPrevLine.x + spacing_w
|
|
window.DC.CursorPos.y = window.DC.CursorPosPrevLine.y
|
|
end
|
|
ImVec2_Copy(window.DC.CurrLineSize, window.DC.PrevLineSize)
|
|
window.DC.CurrLineTextBaseOffset = window.DC.PrevLineTextBaseOffset
|
|
window.DC.IsSameLine = true
|
|
end
|
|
|
|
--- @param indent_w? float
|
|
function ImGui.Indent(indent_w)
|
|
if indent_w == nil then indent_w = 0.0 end
|
|
|
|
local g = GImGui
|
|
local window = ImGui.GetCurrentWindow()
|
|
|
|
if indent_w ~= 0.0 then
|
|
window.DC.Indent.x = window.DC.Indent.x + indent_w
|
|
else
|
|
window.DC.Indent.x = window.DC.Indent.x + g.Style.IndentSpacing
|
|
end
|
|
|
|
window.DC.CursorPos.x = window.Pos.x + window.DC.Indent.x + window.DC.ColumnsOffset.x
|
|
end
|
|
|
|
--- @param indent_w? float
|
|
function ImGui.Unindent(indent_w)
|
|
if indent_w == nil then indent_w = 0.0 end
|
|
|
|
local g = GImGui
|
|
local window = ImGui.GetCurrentWindow()
|
|
|
|
if indent_w ~= 0.0 then
|
|
window.DC.Indent.x = window.DC.Indent.x - indent_w
|
|
else
|
|
window.DC.Indent.x = window.DC.Indent.x - g.Style.IndentSpacing
|
|
end
|
|
|
|
window.DC.CursorPos.x = window.Pos.x + window.DC.Indent.x + window.DC.ColumnsOffset.x
|
|
end
|
|
|
|
-- Affect large frame+labels widgets only.
|
|
--- @param item_width float
|
|
function ImGui.SetNextItemWidth(item_width)
|
|
local g = GImGui
|
|
g.NextItemData.HasFlags = bit32.bor(g.NextItemData.HasFlags, ImGuiNextItemDataFlags.HasWidth)
|
|
g.NextItemData.Width = item_width
|
|
end
|
|
|
|
-- FIXME: Remove the == 0.0f behavior?
|
|
--- @param item_width float
|
|
function ImGui.PushItemWidth(item_width)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
window.DC.ItemWidthStack:push_back(window.DC.ItemWidth) -- Backup current width
|
|
window.DC.ItemWidth = (item_width == 0.0) and window.DC.ItemWidthDefault or item_width
|
|
g.NextItemData.HasFlags = bit32.band(g.NextItemData.HasFlags, bit32.bnot(ImGuiNextItemDataFlags.HasWidth))
|
|
end
|
|
|
|
function ImGui.PopItemWidth()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if window.DC.ItemWidthStack.Size <= 0 then
|
|
IM_ASSERT_USER_ERROR(false, "Calling PopItemWidth() too many times!")
|
|
return
|
|
end
|
|
window.DC.ItemWidth = window.DC.ItemWidthStack:back()
|
|
window.DC.ItemWidthStack:pop_back()
|
|
end
|
|
|
|
--- @param components int
|
|
--- @param w_full float
|
|
function ImGui.PushMultiItemsWidths(components, w_full)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT(components > 0)
|
|
local style = g.Style
|
|
window.DC.ItemWidthStack:push_back(window.DC.ItemWidth) -- Backup current width
|
|
local w_items = w_full - style.ItemInnerSpacing.x * (components - 1)
|
|
local prev_split = w_items
|
|
for i = components - 1, 1, -1 do
|
|
local next_split = IM_TRUNC(w_items * i / components)
|
|
window.DC.ItemWidthStack:push_back(ImMax(prev_split - next_split, 1.0))
|
|
prev_split = next_split
|
|
end
|
|
window.DC.ItemWidth = ImMax(prev_split, 1.0)
|
|
g.NextItemData.HasFlags = bit32.band(g.NextItemData.HasFlags, bit32.bnot(ImGuiNextItemDataFlags.HasWidth))
|
|
end
|
|
|
|
--- @return float
|
|
function ImGui.GetFrameHeight()
|
|
local g = GImGui
|
|
return g.FontSize + g.Style.FramePadding.y * 2.0
|
|
end
|
|
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
@native
|
|
function ImGui.GetContentRegionAvail()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
local mx
|
|
if window.DC.CurrentColumns or g.CurrentTable then
|
|
mx = window.WorkRect.Max
|
|
else
|
|
mx = window.ContentRegionRect.Max
|
|
end
|
|
|
|
return ImVec2(mx.x - window.DC.CursorPos.x, mx.y - window.DC.CursorPos.y)
|
|
end
|
|
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
function ImGui.GetCursorScreenPos()
|
|
local window = ImGui.GetCurrentWindowRead()
|
|
local ret = ImVec2()
|
|
ImVec2_Copy(ret, window.DC.CursorPos)
|
|
return ret
|
|
end
|
|
|
|
--- @param pos ImVec2
|
|
function ImGui.SetCursorScreenPos(pos)
|
|
local window = ImGui.GetCurrentWindow()
|
|
ImVec2_Copy(window.DC.CursorPos, pos)
|
|
-- window.DC.CursorMaxPos = ImMax(window.DC.CursorMaxPos, window.DC.CursorPos)
|
|
window.DC.IsSetPos = true
|
|
end
|
|
|
|
-- User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient.
|
|
-- Conversion happens as we pass the value to user, but it makes our naming convention confusing because GetCursorPos() == (DC.CursorPos - window.Pos). May want to rename 'DC.CursorPos'.
|
|
--- @return ImVec2
|
|
function ImGui.GetCursorPos()
|
|
local window = ImGui.GetCurrentWindowRead()
|
|
return ImVec2(window.DC.CursorPos.x - window.Pos.x + window.Scroll.x, window.DC.CursorPos.y - window.Pos.y + window.Scroll.y)
|
|
end
|
|
|
|
function ImGui.GetCursorPosX()
|
|
local window = ImGui.GetCurrentWindowRead()
|
|
return window.DC.CursorPos.x - window.Pos.x + window.Scroll.x
|
|
end
|
|
|
|
function ImGui.GetCursorPosY()
|
|
local window = ImGui.GetCurrentWindowRead()
|
|
return window.DC.CursorPos.y - window.Pos.y + window.Scroll.y
|
|
end
|
|
|
|
--- @param local_pos ImVec2
|
|
function ImGui.SetCursorPos(local_pos)
|
|
local window = ImGui.GetCurrentWindow()
|
|
ImVec2_CopyV(window.DC.CursorPos, window.Pos.x - window.Scroll.x + local_pos.x, window.Pos.y - window.Scroll.y + local_pos.y)
|
|
-- window.DC.CursorMaxPos = ImMax(window.DC.CursorMaxPos, window.DC.CursorPos)
|
|
window.DC.IsSetPos = true
|
|
end
|
|
|
|
--- @param x float
|
|
function ImGui.SetCursorPosX(x)
|
|
local window = ImGui.GetCurrentWindow()
|
|
window.DC.CursorPos.x = window.Pos.x - window.Scroll.x + x
|
|
-- window.DC.CursorMaxPos.x = ImMax(window.DC.CursorMaxPos.x, window.DC.CursorPos.x)
|
|
window.DC.IsSetPos = true
|
|
end
|
|
|
|
--- @param y float
|
|
function ImGui.SetCursorPosY(y)
|
|
local window = ImGui.GetCurrentWindow()
|
|
window.DC.CursorPos.y = window.Pos.y - window.Scroll.y + y
|
|
-- window.DC.CursorMaxPos.y = ImMax(window.DC.CursorMaxPos.y, window.DC.CursorPos.y)
|
|
window.DC.IsSetPos = true
|
|
end
|
|
|
|
--- @return ImVec2
|
|
function ImGui.GetCursorStartPos()
|
|
local window = ImGui.GetCurrentWindowRead()
|
|
return window.DC.CursorStartPos - window.Pos
|
|
end
|
|
|
|
function ImGui.GetFrameHeightWithSpacing()
|
|
local g = GImGui
|
|
return g.FontSize + g.Style.FramePadding.y * 2.0 + g.Style.ItemSpacing.y
|
|
end
|
|
|
|
function ImGui.GetTextLineHeightWithSpacing()
|
|
local g = GImGui
|
|
return g.FontSize + g.Style.ItemSpacing.y
|
|
end
|
|
|
|
-- You should never need those functions. Always use GetCursorScreenPos() and GetContentRegionAvail()!
|
|
-- They are bizarre local-coordinates which don't play well with scrolling.
|
|
--- @return ImVec2
|
|
function ImGui.GetContentRegionMax()
|
|
local window = GImGui.CurrentWindow
|
|
return ImGui.GetContentRegionAvail() + window.DC.CursorPos - window.Pos
|
|
end
|
|
|
|
--- @return ImVec2
|
|
function ImGui.GetWindowContentRegionMin()
|
|
local window = GImGui.CurrentWindow
|
|
return window.ContentRegionRect.Min - window.Pos
|
|
end
|
|
|
|
--- @return ImVec2
|
|
function ImGui.GetWindowContentRegionMax()
|
|
local window = GImGui.CurrentWindow
|
|
return window.ContentRegionRect.Max - window.Pos
|
|
end
|
|
|
|
-- Overloads: IsRectVisible(size) / IsRectVisible(rect_min, rect_max)
|
|
function ImGui.IsRectVisible(a, b)
|
|
local window = GImGui.CurrentWindow
|
|
if b == nil then
|
|
return window.ClipRect:Overlaps(ImRect(window.DC.CursorPos, window.DC.CursorPos + a))
|
|
end
|
|
return window.ClipRect:Overlaps(ImRect(a, b))
|
|
end
|
|
|
|
-- Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.)
|
|
-- Groups are currently a mishmash of functionalities which should perhaps be clarified and separated.
|
|
-- FIXME-OPT: Could we safely early out on ->SkipItems?
|
|
@native
|
|
function ImGui.BeginGroup()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
local group_data = ImGuiGroupData()
|
|
group_data.WindowID = window.ID
|
|
ImVec2_Copy(group_data.BackupCursorPos, window.DC.CursorPos)
|
|
ImVec2_Copy(group_data.BackupCursorPosPrevLine, window.DC.CursorPosPrevLine)
|
|
ImVec2_Copy(group_data.BackupCursorMaxPos, window.DC.CursorMaxPos)
|
|
ImVec1_Copy(group_data.BackupIndent, window.DC.Indent)
|
|
ImVec1_Copy(group_data.BackupGroupOffset, window.DC.GroupOffset)
|
|
ImVec2_Copy(group_data.BackupCurrLineSize, window.DC.CurrLineSize)
|
|
group_data.BackupCurrLineTextBaseOffset = window.DC.CurrLineTextBaseOffset
|
|
group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive
|
|
group_data.BackupHoveredIdIsAlive = (g.HoveredId ~= 0)
|
|
group_data.BackupIsSameLine = window.DC.IsSameLine
|
|
group_data.BackupAnyIdHasBeenEditedThisFrame = g.AnyIdHasBeenEditedThisFrame
|
|
group_data.BackupDeactivatedIdIsAlive = g.DeactivatedItemData.IsAlive
|
|
group_data.EmitItem = true
|
|
|
|
g.GroupStack:push_back(group_data)
|
|
|
|
window.DC.GroupOffset.x = window.DC.CursorPos.x - window.Pos.x - window.DC.ColumnsOffset.x
|
|
ImVec1_Copy(window.DC.Indent, window.DC.GroupOffset)
|
|
ImVec2_Copy(window.DC.CursorMaxPos, window.DC.CursorPos)
|
|
ImVec2_CopyV(window.DC.CurrLineSize, 0.0, 0.0)
|
|
if (g.LogEnabled) then
|
|
g.LogLinePosY = -FLT_MAX -- To enforce a carriage return
|
|
end
|
|
end
|
|
|
|
@native
|
|
function ImGui.EndGroup()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT(g.GroupStack.Size > 0) -- Mismatched BeginGroup()/EndGroup() calls
|
|
|
|
local group_data = g.GroupStack:back()
|
|
IM_ASSERT(group_data.WindowID == window.ID) -- EndGroup() in wrong window?
|
|
|
|
if (window.DC.IsSetPos) then
|
|
-- TODO: ImGui.ErrorCheckUsingSetCursorPosToExtendParentBoundaries()
|
|
end
|
|
|
|
-- Include LastItemData.Rect.Max as a workaround for e.g. EndTable() undershooting with CursorMaxPos report. (#7543)
|
|
local group_bb = ImRect(group_data.BackupCursorPos, ImMax(ImMax(window.DC.CursorMaxPos, g.LastItemData.Rect.Max), group_data.BackupCursorPos))
|
|
ImVec2_Copy(window.DC.CursorPos, group_data.BackupCursorPos)
|
|
ImVec2_Copy(window.DC.CursorPosPrevLine, group_data.BackupCursorPosPrevLine)
|
|
ImVec2_Copy(window.DC.CursorMaxPos, ImMax(group_data.BackupCursorMaxPos, group_bb.Max))
|
|
ImVec1_Copy(window.DC.Indent, group_data.BackupIndent)
|
|
ImVec1_Copy(window.DC.GroupOffset, group_data.BackupGroupOffset)
|
|
ImVec2_Copy(window.DC.CurrLineSize, group_data.BackupCurrLineSize)
|
|
window.DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset
|
|
window.DC.IsSameLine = group_data.BackupIsSameLine
|
|
if (g.LogEnabled) then
|
|
g.LogLinePosY = -FLT_MAX -- To enforce a carriage return
|
|
end
|
|
|
|
if (not group_data.EmitItem) then
|
|
g.GroupStack:pop_back()
|
|
return
|
|
end
|
|
|
|
window.DC.CurrLineTextBaseOffset = ImMax(window.DC.PrevLineTextBaseOffset, group_data.BackupCurrLineTextBaseOffset) -- FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now.
|
|
ImGui.ItemSize(group_bb:GetSize())
|
|
ImGui.ItemAdd(group_bb, 0, nil, ImGuiItemFlags.NoTabStop)
|
|
|
|
-- If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive(), IsItemDeactivated() etc. will be functional on the entire group.
|
|
-- It would be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but would put a little more burden on individual widgets.
|
|
-- Also if you grep for LastItemId you'll notice it is only used in that context.
|
|
-- (The two tests not the same because ActiveIdIsAlive is an ID itself, in order to be able to handle ActiveId being overwritten during the frame.)
|
|
local group_contains_curr_active_id = (group_data.BackupActiveIdIsAlive ~= g.ActiveId) and (g.ActiveIdIsAlive == g.ActiveId) and g.ActiveId
|
|
local group_contains_deactivated_id = (group_data.BackupDeactivatedIdIsAlive == false) and (g.DeactivatedItemData.IsAlive == true)
|
|
if group_contains_curr_active_id then
|
|
g.LastItemData.ID = g.ActiveId
|
|
elseif group_contains_deactivated_id then
|
|
g.LastItemData.ID = g.DeactivatedItemData.ID
|
|
end
|
|
ImRect_Copy(g.LastItemData.Rect, group_bb)
|
|
|
|
-- Forward Hovered flag
|
|
local group_contains_curr_hovered_id = (group_data.BackupHoveredIdIsAlive == false) and g.HoveredId ~= 0
|
|
if group_contains_curr_hovered_id then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredWindow)
|
|
end
|
|
|
|
-- Forward Edited flag
|
|
if g.AnyIdHasBeenEditedThisFrame and not group_data.BackupAnyIdHasBeenEditedThisFrame then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Edited)
|
|
end
|
|
|
|
-- Forward Deactivated flag
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HasDeactivated)
|
|
if group_contains_deactivated_id then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Deactivated)
|
|
end
|
|
|
|
g.GroupStack:pop_back()
|
|
if g.DebugShowGroupRects then
|
|
window.DrawList:AddRect(group_bb.Min, group_bb.Max, IM_COL32(255, 0, 255, 255)) -- [Debug]
|
|
end
|
|
end
|
|
|
|
-- Calculate default item width given value passed to PushItemWidth() or SetNextItemWidth().
|
|
-- The SetNextItemWidth() data is generally cleared/consumed by ItemAdd() or NextItemData.ClearFlags()
|
|
@native
|
|
function ImGui.CalcItemWidth()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
local w
|
|
if bit32.band(g.NextItemData.HasFlags, ImGuiNextItemDataFlags.HasWidth) ~= 0 then
|
|
w = g.NextItemData.Width
|
|
else
|
|
w = window.DC.ItemWidth
|
|
end
|
|
|
|
if w < 0.0 then
|
|
local region_avail_x = ImGui.GetContentRegionAvail().x
|
|
w = ImMax(1.0, region_avail_x + w)
|
|
end
|
|
|
|
w = IM_TRUNC(w)
|
|
return w
|
|
end
|
|
|
|
-- [Internal] Calculate full item size given user provided 'size' parameter and default width/height. Default width is often == CalcItemWidth().
|
|
-- Those two functions CalcItemWidth vs CalcItemSize are awkwardly named because they are not fully symmetrical.
|
|
-- Note that only CalcItemWidth() is publicly exposed.
|
|
-- The 4.0f here may be changed to match CalcItemWidth() and/or BeginChild() (right now we have a mismatch which is harmless but undesirable)
|
|
--- @param size ImVec2
|
|
--- @param default_w float
|
|
--- @param default_h float
|
|
--- @return ImVec2
|
|
function ImGui.CalcItemSize(size, default_w, default_h)
|
|
local avail
|
|
if size.x < 0.0 or size.y < 0.0 then
|
|
avail = ImGui.GetContentRegionAvail()
|
|
end
|
|
|
|
if size.x == 0.0 then
|
|
size.x = default_w
|
|
elseif size.x < 0.0 then
|
|
size.x = ImMax(4.0, avail.x + size.x) -- <-- size.x is negative here so we are subtracting
|
|
end
|
|
|
|
if size.y == 0.0 then
|
|
size.y = default_h
|
|
elseif size.y < 0.0 then
|
|
size.y = ImMax(4.0, avail.y + size.y) -- <-- size.y is negative here so we are subtracting
|
|
end
|
|
|
|
return size
|
|
end
|
|
|
|
function ImGui.GetTextLineHeight()
|
|
local g = GImGui
|
|
return g.FontSize
|
|
end
|
|
|
|
function ImGui.IsItemActive()
|
|
local g = GImGui
|
|
|
|
if g.ActiveId ~= 0 then
|
|
return g.ActiveId == g.LastItemData.ID
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- == (GetItemID() == GetFocusID() && GetFocusID() != 0)
|
|
function ImGui.IsItemFocused()
|
|
local g = GImGui
|
|
if g.NavId ~= g.LastItemData.ID or g.NavId == 0 then
|
|
return false
|
|
end
|
|
|
|
-- Special handling for the dummy item after Begin() which represent the title bar or tab.
|
|
-- When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case.
|
|
local window = g.CurrentWindow
|
|
if g.LastItemData.ID == window.ID and window.WriteAccessed then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
function ImGui.IsItemDeactivated()
|
|
local g = GImGui
|
|
if bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HasDeactivated) ~= 0 then
|
|
return bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Deactivated) ~= 0
|
|
end
|
|
return g.DeactivatedItemData.ID == g.LastItemData.ID and g.LastItemData.ID ~= 0 and g.DeactivatedItemData.ElapseFrame >= g.FrameCount
|
|
end
|
|
|
|
-- Since 1.92.9: consider using NoLiveEdit flags if all you need to that values are not written bo backing variable while typing.
|
|
function ImGui.IsItemDeactivatedAfterEdit()
|
|
local g = GImGui
|
|
return ImGui.IsItemDeactivated() and g.DeactivatedItemData.HasBeenEditedBefore
|
|
end
|
|
|
|
function ImGui.IsItemActivated()
|
|
local g = GImGui
|
|
if g.ActiveId ~= 0 then
|
|
if g.ActiveId == g.LastItemData.ID and g.ActiveIdPreviousFrame ~= g.LastItemData.ID then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Important: this can be useful but it is NOT equivalent to the behavior of e.g.Button()!
|
|
-- Most widgets have specific reactions based on mouse-up/down state, mouse position etc.
|
|
--- @param mouse_button? ImGuiMouseButton
|
|
function ImGui.IsItemClicked(mouse_button)
|
|
if mouse_button == nil then mouse_button = 0 end
|
|
return ImGui.IsMouseClicked(mouse_button) and ImGui.IsItemHovered(ImGuiHoveredFlags.None)
|
|
end
|
|
|
|
function ImGui.IsItemToggledOpen()
|
|
return bit32.band(GImGui.LastItemData.StatusFlags, ImGuiItemStatusFlags.ToggledOpen) ~= 0
|
|
end
|
|
|
|
-- Call after a Selectable() or TreeNode() items inside a BeginMultiSelect()/EndMultiSelect() scope.
|
|
-- - Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose.
|
|
-- Outside of a multi-select block:
|
|
-- - It would be misleading/ambiguous to report this signal, as widgets return e.g. a pressed event,
|
|
-- and user code is in charge of altering selection in ways we cannot predict.
|
|
-- Prefer using 'if (IsItemClicked() && !IsItemToggledOpen())' for a manual reimplementation of selection.
|
|
function ImGui.IsItemToggledSelection()
|
|
local g = GImGui
|
|
IM_ASSERT(g.CurrentMultiSelect ~= nil) -- Can only be used inside a BeginMultiSelect()/EndMultiSelect()
|
|
return bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.ToggledSelection) ~= 0
|
|
end
|
|
|
|
-- IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app,
|
|
-- you should not use this function! Use the 'io.WantCaptureMouse' boolean for that!
|
|
-- Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details.
|
|
function ImGui.IsAnyItemHovered()
|
|
local g = GImGui
|
|
return g.HoveredId ~= 0 or g.HoveredIdPreviousFrame ~= 0
|
|
end
|
|
|
|
function ImGui.IsAnyItemActive()
|
|
return GImGui.ActiveId ~= 0
|
|
end
|
|
|
|
function ImGui.IsAnyItemFocused()
|
|
local g = GImGui
|
|
return g.NavId ~= 0 and g.NavCursorVisible
|
|
end
|
|
|
|
function ImGui.IsItemVisible()
|
|
return bit32.band(GImGui.LastItemData.StatusFlags, ImGuiItemStatusFlags.Visible) ~= 0
|
|
end
|
|
|
|
function ImGui.IsItemEdited()
|
|
return bit32.band(GImGui.LastItemData.StatusFlags, ImGuiItemStatusFlags.Edited) ~= 0
|
|
end
|
|
|
|
-- Allow next item to be overlapped by subsequent items.
|
|
-- This works by requiring HoveredId to match for two subsequent frames,
|
|
-- so if a following items overwrite it our interactions will naturally be disabled.
|
|
function ImGui.SetNextItemAllowOverlap()
|
|
local g = GImGui
|
|
g.NextItemData.ItemFlagsSet = bit32.bor(g.NextItemData.ItemFlagsSet, ImGuiItemFlags.AllowOverlap)
|
|
end
|
|
|
|
function ImGui.GetItemID()
|
|
return GImGui.LastItemData.ID
|
|
end
|
|
|
|
function ImGui.GetItemFlags()
|
|
return GImGui.LastItemData.ItemFlags
|
|
end
|
|
|
|
--- @return ImVec2
|
|
function ImGui.GetItemRectSize()
|
|
return GImGui.LastItemData.Rect:GetSize()
|
|
end
|
|
|
|
function ImGui.GetHoveredID()
|
|
local g = GImGui
|
|
return (g.HoveredId ~= 0) and g.HoveredId or g.HoveredIdPreviousFrame
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] WINDOW FOCUS
|
|
-------------------------------------------------------------------------------
|
|
-- - SetWindowFocus()
|
|
-- - SetNextWindowFocus()
|
|
-- - IsWindowFocused()
|
|
-- - UpdateWindowInFocusOrderList() [Internal]
|
|
-- - BringWindowToFocusFront() [Internal]
|
|
-- - BringWindowToDisplayFront() [Internal]
|
|
-- - BringWindowToDisplayBack() [Internal]
|
|
-- - BringWindowToDisplayBehind() [Internal]
|
|
-- - FindWindowDisplayIndex() [Internal]
|
|
-- - FocusWindow() [Internal]
|
|
-- - FocusTopMostWindowUnderOne() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Moving window to front of display and set focus (which happens to be back of our sorted list)
|
|
--- @param window? ImGuiWindow
|
|
--- @param flags? ImGuiFocusRequestFlags
|
|
function ImGui.FocusWindow(window, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui
|
|
|
|
-- Modal check?
|
|
if bit32.band(flags, ImGuiFocusRequestFlags.UnlessBelowModal) ~= 0 and g.NavWindow ~= window then -- Early out in common case.
|
|
local blocking_modal = ImGui.FindBlockingModal(window)
|
|
if blocking_modal then
|
|
-- This block would typically be reached in two situations:
|
|
-- - API call to FocusWindow() with a window under a modal and ImGuiFocusRequestFlags.UnlessBelowModal flag.
|
|
-- - User clicking on void or anything behind a modal while a modal is open (window == nil)
|
|
-- IMGUI_DEBUG_LOG_FOCUS("[focus] FocusWindow(\"%s\", UnlessBelowModal): prevented by \"%s\".", window ? window->Name : "<NULL>", blocking_modal->Name)
|
|
if window and window == window.RootWindow and bit32.band(window.Flags, ImGuiWindowFlags.NoBringToFrontOnFocus) == 0 then
|
|
ImGui.BringWindowToDisplayBehind(window, blocking_modal) -- Still bring right under modal. (FIXME: Could move in focus list too?)
|
|
end
|
|
ImGui.ClosePopupsOverWindow(ImGui.GetTopMostPopupModal(), false) -- Note how we need to use GetTopMostPopupModal() aad NOT blocking_modal, to handle nested modals
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Find last focused child (if any) and focus it instead.
|
|
if bit32.band(flags, ImGuiFocusRequestFlags.RestoreFocusedChild) ~= 0 and window ~= nil then
|
|
window = ImGui.NavRestoreLastChildNavWindow(window)
|
|
end
|
|
|
|
-- Apply focus
|
|
if g.NavWindow ~= window then
|
|
ImGui.SetNavWindow(window)
|
|
if window and g.NavHighlightItemUnderNav then
|
|
g.NavMousePosDirty = true
|
|
end
|
|
g.NavId = window and window.NavLastIds[0] or 0 -- Restore NavId
|
|
g.NavLayer = ImGuiNavLayer.Main
|
|
ImGui.SetNavFocusScope(window and window.NavRootFocusScopeId or 0)
|
|
g.NavIdIsAlive = false
|
|
g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid
|
|
|
|
-- Close popups if any
|
|
ImGui.ClosePopupsOverWindow(window, false)
|
|
end
|
|
|
|
-- Move the root window to the top of the pile
|
|
IM_ASSERT(window == nil or window.RootWindowDockTree ~= nil)
|
|
local focus_front_window = window and window.RootWindow or nil
|
|
local display_front_window = window and window.RootWindowDockTree or nil
|
|
local dock_node = window and window.DockNode or nil
|
|
local active_id_window_is_dock_node_host = g.ActiveIdWindow and dock_node and dock_node.HostWindow == g.ActiveIdWindow
|
|
|
|
-- Steal active widgets. Some of the cases it triggers includes:
|
|
-- - Focus a window while an InputText in another window is active, if focus happens before the old InputText can run.
|
|
-- - When using Nav to activate menu items (due to timing of activating on press->new window appears->losing ActiveId)
|
|
-- - Using dock host items (tab, collapse button) can trigger this before we redirect the ActiveIdWindow toward the child window.
|
|
if g.ActiveId ~= 0 and g.ActiveIdWindow and g.ActiveIdWindow.RootWindow ~= focus_front_window then
|
|
if not g.ActiveIdNoClearOnFocusLoss and not active_id_window_is_dock_node_host then
|
|
ImGui.ClearActiveID()
|
|
end
|
|
end
|
|
|
|
-- Passing nil allow to disable keyboard focus
|
|
if not window then
|
|
return
|
|
end
|
|
window.LastFrameJustFocused = g.FrameCount
|
|
|
|
-- Select in dock node
|
|
-- For #2304 we avoid applying focus immediately before the tabbar is visible.
|
|
-- if (dock_node && dock_node->TabBar)
|
|
-- dock_node->TabBar->SelectedTabId = dock_node->TabBar->NextSelectedTabId = window->TabId;
|
|
|
|
-- Bring to front
|
|
ImGui.BringWindowToFocusFront(focus_front_window)
|
|
if bit32.band(bit32.bor(window.Flags, focus_front_window.Flags, display_front_window.Flags), ImGuiWindowFlags.NoBringToFrontOnFocus) == 0 then
|
|
ImGui.BringWindowToDisplayFront(display_front_window)
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return int
|
|
function ImGui.FindWindowFocusIndex(window)
|
|
local g = GImGui
|
|
-- IM_UNUSED(g)
|
|
local order = window.FocusOrder
|
|
IM_ASSERT(window.RootWindow == window) -- No child window (not testing _ChildWindow because of docking)
|
|
IM_ASSERT(g.WindowsFocusOrder.Data[order] == window)
|
|
return order
|
|
end
|
|
|
|
-- Can we focus this window with Ctrl+Tab (or PadMenu + PadFocusPrev/PadFocusNext)
|
|
-- Note that NoNavFocus makes the window not reachable with Ctrl+Tab but it can still be focused with mouse or programmatically.
|
|
-- If you want a window to never be focused, you may use the e.g. NoInputs flag.
|
|
--- @param window ImGuiWindow
|
|
--- @return bool
|
|
function ImGui.IsWindowNavFocusable(window)
|
|
return window.WasActive and window == window.RootWindow and bit32.band(window.Flags, ImGuiWindowFlags.NoNavFocus) == 0
|
|
end
|
|
|
|
--- @param i_start int
|
|
--- @param i_stop int
|
|
--- @param dir int
|
|
--- @return ImGuiWindow?
|
|
function ImGui.FindWindowNavFocusable(i_start, i_stop, dir) -- FIXME-OPT O(N)
|
|
local g = GImGui
|
|
local i = i_start
|
|
while i >= 1 and i <= g.WindowsFocusOrder.Size and i ~= i_stop do
|
|
local window = g.WindowsFocusOrder.Data[i]
|
|
if ImGui.IsWindowNavFocusable(window) then
|
|
return window
|
|
end
|
|
i = i + dir
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ImGui.UpdateWindowInFocusOrderList(window, just_created, new_flags)
|
|
local g = GImGui
|
|
|
|
local new_is_explicit_child = (bit32.band(new_flags, ImGuiWindowFlags.ChildWindow) ~= 0) and ((bit32.band(new_flags, ImGuiWindowFlags.Popup) == 0) or (bit32.band(new_flags, ImGuiWindowFlags.ChildMenu) ~= 0))
|
|
local child_flag_changed = (new_is_explicit_child ~= window.IsExplicitChild)
|
|
|
|
if (just_created or child_flag_changed) and not new_is_explicit_child then
|
|
IM_ASSERT(not g.WindowsFocusOrder:contains(window))
|
|
g.WindowsFocusOrder:push_back(window)
|
|
window.FocusOrder = g.WindowsFocusOrder.Size
|
|
elseif not just_created and child_flag_changed and new_is_explicit_child then
|
|
IM_ASSERT(g.WindowsFocusOrder.Data[window.FocusOrder] == window)
|
|
|
|
for n = window.FocusOrder + 1, g.WindowsFocusOrder.Size do
|
|
g.WindowsFocusOrder.Data[n].FocusOrder = g.WindowsFocusOrder.Data[n].FocusOrder - 1
|
|
end
|
|
|
|
g.WindowsFocusOrder:erase(window.FocusOrder)
|
|
window.FocusOrder = -1
|
|
end
|
|
|
|
window.IsExplicitChild = new_is_explicit_child
|
|
end
|
|
|
|
--- @param window? ImGuiWindow
|
|
function ImGui.BringWindowToFocusFront(window)
|
|
local g = GImGui
|
|
IM_ASSERT(window == window.RootWindow)
|
|
|
|
local cur_order = window.FocusOrder
|
|
IM_ASSERT(g.WindowsFocusOrder.Data[cur_order] == window)
|
|
|
|
if g.WindowsFocusOrder:back() == window then
|
|
return
|
|
end
|
|
|
|
local new_order = g.WindowsFocusOrder.Size
|
|
|
|
for n = cur_order, new_order - 1 do
|
|
g.WindowsFocusOrder.Data[n] = g.WindowsFocusOrder.Data[n + 1]
|
|
g.WindowsFocusOrder.Data[n].FocusOrder = g.WindowsFocusOrder.Data[n].FocusOrder - 1
|
|
IM_ASSERT(g.WindowsFocusOrder.Data[n].FocusOrder == n)
|
|
end
|
|
|
|
g.WindowsFocusOrder.Data[new_order] = window
|
|
window.FocusOrder = new_order
|
|
end
|
|
|
|
-- Note technically focus related but rather adjacent and close to BringWindowToFocusFront()
|
|
-- FIXME-FOCUS: Could opt-in/opt-out enable modal check like in FocusWindow().
|
|
--- @param window? ImGuiWindow
|
|
function ImGui.BringWindowToDisplayFront(window)
|
|
local g = GImGui
|
|
|
|
local current_front_window = g.Windows:back()
|
|
|
|
if current_front_window == window or current_front_window.RootWindowDockTree == window then -- Cheap early out (could be better)
|
|
return
|
|
end
|
|
|
|
for i, this_window in g.Windows:iter() do
|
|
if this_window == window then
|
|
g.Windows:erase(i)
|
|
break
|
|
end
|
|
end
|
|
|
|
g.Windows:push_back(window)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param behind_window ImGuiWindow
|
|
function ImGui.BringWindowToDisplayBehind(window, behind_window)
|
|
IM_ASSERT(window ~= nil and behind_window ~= nil)
|
|
local g = GImGui
|
|
window = window.RootWindow
|
|
behind_window = behind_window.RootWindow
|
|
-- TODO:
|
|
end
|
|
|
|
--- @param under_this_window? ImGuiWindow
|
|
--- @param ignore_window? ImGuiWindow
|
|
--- @param filter_viewport? ImGuiViewport
|
|
--- @param flags ImGuiFocusRequestFlags
|
|
function ImGui.FocusTopMostWindowUnderOne(under_this_window, ignore_window, filter_viewport, flags)
|
|
local g = GImGui
|
|
local start_idx = g.WindowsFocusOrder.Size
|
|
if under_this_window ~= nil then
|
|
-- Aim at root window behind us, if we are in a child window that's our own root (see #4640)
|
|
local offset = -1
|
|
while bit32.band(under_this_window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 do
|
|
under_this_window = under_this_window.ParentWindow
|
|
offset = 0
|
|
end
|
|
--- @cast under_this_window ImGuiWindow
|
|
start_idx = ImGui.FindWindowFocusIndex(under_this_window) + offset
|
|
end
|
|
for i = start_idx, 1, -1 do
|
|
repeat
|
|
-- We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user.
|
|
local window = g.WindowsFocusOrder.Data[i]
|
|
if window == ignore_window or not window.WasActive then
|
|
break
|
|
end
|
|
if filter_viewport ~= nil and window.Viewport ~= filter_viewport then
|
|
break
|
|
end
|
|
if bit32.band(window.Flags, bit32.bor(ImGuiWindowFlags.NoMouseInputs, ImGuiWindowFlags.NoNavInputs)) ~= bit32.bor(ImGuiWindowFlags.NoMouseInputs, ImGuiWindowFlags.NoNavInputs) then
|
|
-- FIXME-DOCK: When ImGuiFocusRequestFlags.RestoreFocusedChild is set...
|
|
-- This is failing (lagging by one frame) for docked windows.
|
|
-- If A and B are docked into window and B disappear, at the NewFrame() call site window.NavLastChildNavWindow will still point to B.
|
|
-- We might leverage the tab order implicitly stored in window.DockNodeAsHost.TabBar (essentially the 'most_recently_selected_tab' code in tab bar will do that but on next update)
|
|
-- to tell which is the "previous" window. Or we may leverage 'LastFrameFocused/LastFrameJustFocused' and have this function handle child window itself?
|
|
ImGui.FocusWindow(window, flags)
|
|
return
|
|
end
|
|
|
|
until true
|
|
end
|
|
ImGui.FocusWindow(nil, flags)
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
--- @param window ImGuiWindow
|
|
function ImGui.SetFocusID(id, window)
|
|
local g = GImGui
|
|
IM_ASSERT(id ~= 0)
|
|
|
|
if g.NavWindow ~= window then
|
|
ImGui.SetNavWindow(window)
|
|
end
|
|
|
|
-- Assume that SetFocusID() is called in the context where its window.DC.NavLayerCurrent and g.CurrentFocusScopeId are valid.
|
|
-- Note that window may be ~= g.CurrentWindow (e.g. SetFocusID call in InputTextEx for multi-line text)
|
|
local nav_layer = window.DC.NavLayerCurrent
|
|
g.NavId = id
|
|
g.NavLayer = nav_layer
|
|
ImGui.SetNavFocusScope(g.CurrentFocusScopeId)
|
|
window.NavLastIds[nav_layer] = id
|
|
if g.LastItemData.ID == id then
|
|
ImRect_Copy(window.NavRectRel[nav_layer], ImGui.WindowRectAbsToRel(window, g.LastItemData.NavRect))
|
|
end
|
|
g.NavIdItemFlags = (g.LastItemData.ID == id) and g.LastItemData.ItemFlags or ImGuiItemFlags.None
|
|
if id == g.ActiveIdIsAlive then
|
|
g.NavIdIsAlive = true
|
|
end
|
|
|
|
if g.ActiveIdSource == ImGuiInputSource.Keyboard or g.ActiveIdSource == ImGuiInputSource.Gamepad then
|
|
g.NavHighlightItemUnderNav = true
|
|
elseif g.IO.ConfigNavCursorVisibleAuto then
|
|
g.NavCursorVisible = false
|
|
end
|
|
|
|
-- Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it)
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.X)
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.Y)
|
|
end
|
|
|
|
-- This is not 100% symmetric with StartMouseMovingWindow().
|
|
-- We do NOT clear ActiveID, because:
|
|
-- - It would lead to rather confusing recursive code paths. Caller can call ClearActiveID() if desired.
|
|
-- - Some code intentionally cancel moving but keep the ActiveID to lock inputs (e.g. code path taken when clicking a disabled item).
|
|
function ImGui.StopMouseMovingWindow()
|
|
local g = GImGui
|
|
local window = g.MovingWindow
|
|
|
|
-- Ref commits 6b7766817, 36055213c for some partial history on checking if viewport ~= nil.
|
|
if window and window.Viewport then
|
|
-- Try to merge the window back into the main viewport.
|
|
-- This works because MouseViewport should be ~= MovingWindow.Viewport on release (as per code in UpdateViewports)
|
|
if bit32.band(g.ConfigFlagsCurrFrame, ImGuiConfigFlags.ViewportsEnable) ~= 0 then
|
|
ImGui.UpdateTryMergeWindowIntoHostViewport(window.RootWindowDockTree, g.MouseViewport)
|
|
end
|
|
|
|
-- Restore the mouse viewport so that we don't hover the viewport _under_ the moved window during the frame we released the mouse button.
|
|
if (not ImGui.IsDragDropPayloadBeingAccepted()) then
|
|
g.MouseViewport = window.Viewport
|
|
end
|
|
|
|
-- Clear the NoInputs window flag set by the Viewport system in AddUpdateViewport()
|
|
local window_can_use_inputs = bit32.band(window.Flags, ImGuiWindowFlags.NoMouseInputs) == 0 or bit32.band(window.Flags, ImGuiWindowFlags.NoNavInputs) == 0
|
|
if window_can_use_inputs then
|
|
window.Viewport.Flags = bit32.band(window.Viewport.Flags, bit32.bnot(ImGuiViewportFlags.NoInputs))
|
|
end
|
|
end
|
|
|
|
g.MovingWindow = nil
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
--- @param window? ImGuiWindow
|
|
function ImGui.SetActiveID(id, window)
|
|
local g = GImGui
|
|
|
|
-- Clear previous active id
|
|
if g.ActiveId ~= 0 then
|
|
-- Store deactivate data
|
|
local deactivated_data = g.DeactivatedItemData
|
|
deactivated_data.ID = g.ActiveId
|
|
deactivated_data.ElapseFrame = (g.LastItemData.ID == g.ActiveId) and g.FrameCount or g.FrameCount + 1 -- FIXME: OK to use LastItemData?
|
|
deactivated_data.HasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore
|
|
deactivated_data.IsAlive = (g.ActiveIdIsAlive == g.ActiveId)
|
|
|
|
-- This could be written in a more general way (e.g associate a hook to ActiveId),
|
|
-- but since this is currently quite an exception we'll leave it as is.
|
|
-- One common scenario leading to this is: pressing Key ->NavMoveRequestApplyResult() -> ClearActiveID()
|
|
if g.InputTextState.ID == g.ActiveId then
|
|
ImGui.InputTextDeactivateHook(g.ActiveId)
|
|
end
|
|
|
|
-- While most behaved code would make an effort to not steal active id during window move/drag operations,
|
|
-- we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch
|
|
-- may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that.
|
|
if g.MovingWindow and (g.ActiveId == g.MovingWindow.MoveId) then
|
|
print("SetActiveID() cancel MovingWindow")
|
|
ImGui.StopMouseMovingWindow()
|
|
end
|
|
end
|
|
|
|
-- Set active id
|
|
g.ActiveIdIsJustActivated = (g.ActiveId ~= id)
|
|
if (g.ActiveIdIsJustActivated) then
|
|
-- IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() 0x%08X in \"%s\"%*s(previously 0x%08X in \"%s\")\n", id, window ? window->Name : "",
|
|
-- ImMax(0, 20 - (int)(window ? strlen(window->Name) : 0)), "", g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "");
|
|
g.ActiveIdTimer = 0.0
|
|
g.ActiveIdHasBeenPressedBefore = false
|
|
g.ActiveIdHasBeenEditedBefore = false
|
|
g.ActiveIdMouseButton = -1
|
|
|
|
if id ~= 0 then
|
|
g.LastActiveId = id
|
|
g.LastActiveIdTimer = 0.0
|
|
end
|
|
end
|
|
g.ActiveId = id
|
|
g.ActiveIdAllowOverlap = false
|
|
g.ActiveIdNoClearOnFocusLoss = false
|
|
g.ActiveIdWindow = window
|
|
g.ActiveIdHasBeenEditedThisFrame = false
|
|
g.ActiveIdFromShortcut = false
|
|
g.ActiveIdDisabledId = 0
|
|
if id ~= 0 then
|
|
g.ActiveIdIsAlive = id
|
|
if g.NavActivateId == id or g.NavJustMovedToId == id then
|
|
g.ActiveIdSource = g.NavInputSource
|
|
else
|
|
g.ActiveIdSource = ImGuiInputSource.Mouse
|
|
end
|
|
IM_ASSERT(g.ActiveIdSource ~= ImGuiInputSource.None)
|
|
end
|
|
|
|
-- Clear declaration of inputs claimed by the widget
|
|
-- (Please note that this is WIP and not all keys/inputs are thoroughly declared by all widgets yet)
|
|
g.ActiveIdUsingNavDirMask = 0x00
|
|
g.ActiveIdUsingAllKeyboardKeys = false
|
|
end
|
|
|
|
function ImGui.ClearActiveID()
|
|
ImGui.SetActiveID(0, nil) -- g.ActiveId = 0
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] ID STACK
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param str_id string|int
|
|
@native
|
|
function ImGui.PushID(str_id)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local id = window:GetID(str_id)
|
|
window.IDStack:push_back(id)
|
|
end
|
|
|
|
-- Push a given id value ignoring the ID stack as a seed.
|
|
--- @param id ImGuiID
|
|
function ImGui.PushOverrideID(id)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if g.DebugHookIdInfoId == id and id ~= 0 then
|
|
ImGui.DebugHookIdInfo(id, ImGuiDataType.ID, nil)
|
|
end
|
|
window.IDStack:push_back(id)
|
|
end
|
|
|
|
@native
|
|
function ImGui.PopID()
|
|
local window = GImGui.CurrentWindow
|
|
IM_ASSERT_USER_ERROR_RET(window.IDStack.Size > 1, "Calling PopID() too many times!")
|
|
window.IDStack:pop_back()
|
|
end
|
|
|
|
-- This is one of the very rare legacy case where we use ImGuiWindow methods,
|
|
-- it should ideally be flattened at some point but it's been used a lots by widgets.
|
|
--- @param id string|int
|
|
--- @return ImGuiID
|
|
@native
|
|
function MT.ImGuiWindow:GetID(id)
|
|
local seed = self.IDStack:back()
|
|
|
|
local h
|
|
if type(id) == "string" then
|
|
h = ImHashStr(id, nil, seed)
|
|
else --- @cast id int
|
|
h = ImHashData(id, -1, seed)
|
|
end
|
|
local g = GImGui
|
|
if g.DebugHookIdInfoId == h and h ~= 0 then
|
|
ImGui.DebugHookIdInfo(h, (type(id) == "string") and ImGuiDataType.String or ImGuiDataType.S32, id)
|
|
end
|
|
return h
|
|
end
|
|
|
|
-- This is only used in rare/specific situations to manufacture an ID out of nowhere.
|
|
-- FIXME: Consider instead storing last non-zero ID + count of successive zero-ID, and combine those?
|
|
--- @param p_abs ImVec2
|
|
--- @return ImGuiID
|
|
function MT.ImGuiWindow:GetIDFromPos(p_abs)
|
|
local seed = self.IDStack:back()
|
|
local p_rel = ImGui.WindowPosAbsToRel(self, p_abs)
|
|
return ImHashData(p_rel, 2, seed)
|
|
end
|
|
|
|
-- "
|
|
--- @param r_abs ImRect
|
|
--- @return ImGuiID
|
|
function MT.ImGuiWindow:GetIDFromRectangle(r_abs)
|
|
local seed = self.IDStack:back()
|
|
local r_rel = ImGui.WindowRectAbsToRel(self, r_abs)
|
|
return ImHashData({ math.floor(r_rel.Min.x), math.floor(r_rel.Min.y), math.floor(r_rel.Max.x), math.floor(r_rel.Max.y) }, 4, seed)
|
|
end
|
|
|
|
-- Overloads: GetID(str_id) / GetID(str_id_begin, str_id_end) / GetID(int_id)
|
|
--- @param str_or_int string|int
|
|
--- @param str_end? int # exclusive end (1-based byte position)
|
|
--- @return ImGuiID
|
|
function ImGui.GetID(str_or_int, str_end)
|
|
local window = GImGui.CurrentWindow
|
|
if str_end ~= nil and type(str_or_int) == "string" then
|
|
return ImHashStr(str_or_int, str_end - 1, window.IDStack:back())
|
|
end
|
|
return window:GetID(str_or_int)
|
|
end
|
|
|
|
-- Helper to avoid a common series of PushOverrideID -> GetID() -> PopID() call
|
|
-- (note that when using this pattern, ID Stack Tool will tend to not display the intermediate stack level.
|
|
-- for that to work we would need to do PushOverrideID() -> ItemAdd() -> PopID() which would alter widget code a little more)
|
|
-- Overloads: GetIDWithSeed(str, str_end, seed) / GetIDWithSeed(n, seed)
|
|
function ImGui.GetIDWithSeed(a, b, c)
|
|
local id
|
|
if type(a) == "number" then
|
|
id = ImHashData(a, -1, b)
|
|
else
|
|
id = ImHashStr(a, b and (b - 1) or nil, c)
|
|
end
|
|
if GImGui.DebugHookIdInfoId == id and id ~= 0 then
|
|
ImGui.DebugHookIdInfo(id, (type(a) == "number") and ImGuiDataType.S32 or ImGuiDataType.String, (b and type(a) == "string") and string.sub(a, 1, b - 1) or a)
|
|
end
|
|
return id
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
@native
|
|
function ImGui.SetHoveredID(id)
|
|
local g = GImGui
|
|
g.HoveredId = id
|
|
g.HoveredIdAllowOverlap = false
|
|
if id ~= 0 and g.HoveredIdPreviousFrame ~= id then
|
|
g.HoveredIdTimer = 0.0
|
|
g.HoveredIdNotActiveTimer = 0.0
|
|
end
|
|
end
|
|
|
|
--- @param flags ImGuiHoveredFlags
|
|
--- @return float
|
|
local function CalcDelayFromHoveredFlags(flags)
|
|
local g = GImGui
|
|
if bit32.band(flags, ImGuiHoveredFlags.DelayNormal) ~= 0 then
|
|
return g.Style.HoverDelayNormal
|
|
end
|
|
if bit32.band(flags, ImGuiHoveredFlags.DelayShort) ~= 0 then
|
|
return g.Style.HoverDelayShort
|
|
end
|
|
return 0.0
|
|
end
|
|
|
|
--- @param user_flags ImGuiHoveredFlags
|
|
--- @param shared_flags ImGuiHoveredFlags
|
|
local function ApplyHoverFlagsForTooltip(user_flags, shared_flags)
|
|
-- Allow instance flags to override shared flags
|
|
if bit32.band(user_flags, bit32.bor(ImGuiHoveredFlags.DelayNone, ImGuiHoveredFlags.DelayShort, ImGuiHoveredFlags.DelayNormal)) ~= 0 then
|
|
shared_flags = bit32.band(shared_flags, bit32.bnot(bit32.bor(ImGuiHoveredFlags.DelayNone, ImGuiHoveredFlags.DelayShort, ImGuiHoveredFlags.DelayNormal)))
|
|
end
|
|
return bit32.bor(user_flags, shared_flags)
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
function ImGui.MarkItemEdited(id)
|
|
-- This marking is to be able to provide info for IsItemDeactivatedAfterEdit().
|
|
-- ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data.
|
|
local g = GImGui
|
|
|
|
-- IM_ASSERT(g.LastItemData.ID == id) -- Failing cases include: "widgets_inputtext_scrolling", "widgets_inputtext_multiline_status", "widgets_selectable_input" = case of e.g TempInputText() overlayed manually with different ID (#2718)
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.EditedInternal)
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.NoMarkEdited) ~= 0 then
|
|
return
|
|
end
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Edited)
|
|
|
|
if g.ActiveId == id or g.ActiveId == 0 then
|
|
-- FIXME: Can't we fully rely on LastItemData yet?
|
|
g.AnyIdHasBeenEditedThisFrame = true
|
|
g.ActiveIdHasBeenEditedThisFrame = true
|
|
g.ActiveIdHasBeenEditedBefore = true
|
|
end
|
|
if g.DeactivatedItemData.ID == id then
|
|
g.DeactivatedItemData.HasBeenEditedBefore = true
|
|
end
|
|
|
|
-- We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343)
|
|
-- We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714)
|
|
-- FIXME: This assert is getting a bit meaningless over time. It helped detect some unusual use cases but eventually it is becoming an unnecessary restriction.
|
|
IM_ASSERT(g.DragDropActive or g.ActiveId == id or g.ActiveId == 0 or g.DeactivatedItemData.ID == id or g.ActiveIdPreviousFrame == id or g.NavJustMovedToId or (g.CurrentMultiSelect ~= nil and g.BoxSelectState.IsActive))
|
|
|
|
-- IM_ASSERT(g.CurrentWindow.DC.LastItemId == id)
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.Edited)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param flags ImGuiHoveredFlags
|
|
function ImGui.IsWindowContentHoverable(window, flags)
|
|
-- An active popup disable hovering on other windows (apart from its own children)
|
|
-- FIXME-OPT: This could be cached/stored within the window.
|
|
local g = GImGui
|
|
if g.NavWindow then
|
|
local focused_root_window = g.NavWindow.RootWindowDockTree
|
|
if focused_root_window.WasActive and focused_root_window ~= window.RootWindowDockTree then
|
|
-- For the purpose of those flags we differentiate "standard popup" from "modal popup"
|
|
-- NB: The 'else' is important because Modal windows are also Popups.
|
|
local want_inhibit = false
|
|
if bit32.band(focused_root_window.Flags, ImGuiWindowFlags.Modal) ~= 0 then
|
|
want_inhibit = true
|
|
elseif (bit32.band(focused_root_window.Flags, ImGuiWindowFlags.Popup) ~= 0) and (bit32.band(flags, ImGuiHoveredFlags.AllowWhenBlockedByPopup) == 0) then
|
|
want_inhibit = true
|
|
end
|
|
|
|
-- Inhibit hover unless the window is within the stack of our modal/popup
|
|
if want_inhibit then
|
|
if not ImGui.IsWindowWithinBeginStackOf(window.RootWindow, focused_root_window) then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Filter by viewport
|
|
if (window.Viewport ~= g.MouseViewport) then
|
|
if (g.MovingWindow == nil or window.RootWindowDockTree ~= g.MovingWindow.RootWindowDockTree) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- This is roughly matching the behavior of internal-facing ItemHoverable()
|
|
-- - we allow hovering to be true when ActiveId==window.MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered()
|
|
-- - this should work even for non-interactive items that have no ID, so we cannot use LastItemId
|
|
--- @param flags? ImGuiHoveredFlags
|
|
@native
|
|
function ImGui.IsItemHovered(flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT_USER_ERROR(bit32.band(flags, bit32.bnot(ImGuiHoveredFlags.AllowedMaskForIsItemHovered)) == 0, "Invalid flags for IsItemHovered()!")
|
|
|
|
if g.NavHighlightItemUnderNav and g.NavCursorVisible and bit32.band(flags, ImGuiHoveredFlags.NoNavOverride) == 0 then
|
|
if not ImGui.IsItemFocused() then
|
|
return false
|
|
end
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.Disabled) ~= 0 and bit32.band(flags, ImGuiHoveredFlags.AllowWhenDisabled) == 0 then
|
|
return false
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiHoveredFlags.ForTooltip) ~= 0 then
|
|
flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipNav)
|
|
end
|
|
else
|
|
-- Test for bounding box overlap, as updated as ItemAdd()
|
|
local status_flags = g.LastItemData.StatusFlags
|
|
if bit32.band(status_flags, ImGuiItemStatusFlags.HoveredRect) == 0 then
|
|
return false
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiHoveredFlags.ForTooltip) ~= 0 then
|
|
flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse)
|
|
end
|
|
|
|
-- Done with rectangle culling so we can perform heavier checks now
|
|
-- Test if we are hovering the right window (our window could be behind another window)
|
|
-- [2021/03/02] Reworked / reverted the revert, finally. Note we want e.g. BeginGroup/ItemAdd/EndGroup to work as well. (#3851)
|
|
-- [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable
|
|
-- to use IsItemHovered() after EndChild() itself. Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was
|
|
-- the test that has been running for a long while.
|
|
if g.HoveredWindow ~= window and bit32.band(status_flags, ImGuiItemStatusFlags.HoveredWindow) == 0 then
|
|
if bit32.band(flags, ImGuiHoveredFlags.AllowWhenOverlappedByWindow) == 0 then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Test if another item is active (e.g. being dragged)
|
|
local id = g.LastItemData.ID
|
|
if bit32.band(flags, ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) == 0 then
|
|
if g.ActiveId ~= 0 and g.ActiveId ~= id and not g.ActiveIdAllowOverlap and not g.ActiveIdFromShortcut then
|
|
-- When ActiveId == MoveId it means that either:
|
|
-- - (1) user clicked on void _or_ an item with no id, which triggers moving window (ActiveId is set even when window has _NoMove flag)
|
|
-- - the (id == 0) test handles it, however, IsItemHovered() will leak between id==0 items (mostly visible when using _NoMove). -- FIXME: May be fixed.
|
|
-- - (2) user clicked a disabled item. UpdateMouseMovingWindowEndFrame() uses ActiveId == MoveId to avoid interference with item logic + sets ActiveIdDisabledId.
|
|
local cancel_is_hovered = true
|
|
if g.ActiveId == window.MoveId and (id == 0 or g.ActiveIdDisabledId == id) then
|
|
cancel_is_hovered = false
|
|
end
|
|
-- When ActiveId == TabId it means user clicked docking tab for the window.
|
|
if g.ActiveId == window.TabId then
|
|
cancel_is_hovered = false
|
|
end
|
|
if cancel_is_hovered then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Test if interactions on this window are blocked by an active popup or modal.
|
|
-- The ImGuiHoveredFlags.AllowWhenBlockedByPopup flag will be tested here.
|
|
if not ImGui.IsWindowContentHoverable(window, flags) and bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.NoWindowHoverableCheck) == 0 then
|
|
return false
|
|
end
|
|
|
|
-- Test if the item is disabled
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.Disabled) ~= 0 and bit32.band(flags, ImGuiHoveredFlags.AllowWhenDisabled) == 0 then
|
|
return false
|
|
end
|
|
|
|
-- Special handling for calling after Begin() which represent the title bar or tab.
|
|
-- When the window is skipped/collapsed (SkipItems==true) that last item (always .MoveId submitted by Begin)
|
|
-- will never be overwritten so we need to detect the case.
|
|
if id == window.MoveId and window.WriteAccessed then
|
|
return false
|
|
end
|
|
|
|
-- Test if using AllowOverlap and overlapped
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.AllowOverlap) ~= 0 and id ~= 0 then
|
|
if bit32.band(flags, ImGuiHoveredFlags.AllowWhenOverlappedByItem) == 0 then
|
|
if g.HoveredIdPreviousFrame ~= g.LastItemData.ID then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle hover delay
|
|
-- (some ideas: https://www.nngroup.com/articles/timing-exposing-content)
|
|
local delay = CalcDelayFromHoveredFlags(flags)
|
|
if delay > 0.0 or bit32.band(flags, ImGuiHoveredFlags.Stationary) ~= 0 then
|
|
local hover_delay_id
|
|
if g.LastItemData.ID ~= 0 then
|
|
hover_delay_id = g.LastItemData.ID
|
|
else
|
|
hover_delay_id = window:GetIDFromPos(g.LastItemData.Rect.Min)
|
|
end
|
|
if bit32.band(flags, ImGuiHoveredFlags.NoSharedDelay) ~= 0 and g.HoverItemDelayIdPreviousFrame ~= hover_delay_id then
|
|
g.HoverItemDelayTimer = 0.0
|
|
end
|
|
g.HoverItemDelayId = hover_delay_id
|
|
|
|
-- When changing hovered item we requires a bit of stationary delay before activating hover timer,
|
|
-- but once unlocked on a given item we also moving.
|
|
-- if (g.HoverDelayTimer >= delay && (g.HoverDelayTimer - g.IO.DeltaTime < delay || g.MouseStationaryTimer - g.IO.DeltaTime < g.Style.HoverStationaryDelay)) { IMGUI_DEBUG_LOG("HoverDelayTimer = %f/%f, MouseStationaryTimer = %f\n", g.HoverDelayTimer, delay, g.MouseStationaryTimer); }
|
|
if bit32.band(flags, ImGuiHoveredFlags.Stationary) ~= 0 and g.HoverItemUnlockedStationaryId ~= hover_delay_id then
|
|
return false
|
|
end
|
|
|
|
if g.HoverItemDelayTimer < delay then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Internal facing ItemHoverable() used when submitting widgets. THIS IS A SUBMISSION NOT A HOVER CHECK.
|
|
-- Returns whether the item was hovered, logic differs slightly from IsItemHovered().
|
|
-- (this does not rely on LastItemData it can be called from a ButtonBehavior() call not following an ItemAdd() call)
|
|
-- FIXME-LEGACY: the 'ImGuiItemFlags item_flags' parameter was added on 2023-06-28.
|
|
-- If you used this in your legacy/custom widgets code:
|
|
-- - Commonly: if your ItemHoverable() call comes after an ItemAdd() call: pass 'item_flags = g.LastItemData.ItemFlags'.
|
|
-- - Rare: otherwise you may pass 'item_flags = 0' (ImGuiItemFlags.None) unless you want to benefit from special behavior handled by ItemHoverable.
|
|
--- @param bb ImRect
|
|
--- @param id ImGuiID
|
|
--- @param item_flags ImGuiItemFlags
|
|
@native
|
|
function ImGui.ItemHoverable(bb, id, item_flags)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
-- Detect ID conflicts
|
|
-- (this is specifically done here by comparing on hover because it allows us a detection of duplicates that is algorithmically extra cheap, 1 u32 compare per item. No O(log N) lookup whatsoever)
|
|
if not IMGUI_DISABLE_DEBUG_TOOLS then
|
|
if id ~= 0 and g.HoveredIdPreviousFrame == id and bit32.band(item_flags, ImGuiItemFlags.AllowDuplicateId) == 0 then
|
|
g.HoveredIdPreviousFrameItemCount = g.HoveredIdPreviousFrameItemCount + 1
|
|
if g.DebugDrawIdConflictsId == id then
|
|
window.DrawList:AddRect(bb.Min - ImVec2(1, 1), bb.Max + ImVec2(1, 1), IM_COL32(255, 0, 0, 255), 0.0, 2.0, ImDrawFlags.None)
|
|
end
|
|
end
|
|
end
|
|
|
|
if g.HoveredWindow ~= window then
|
|
return false
|
|
end
|
|
|
|
if not ImGui.IsMouseHoveringRect(bb.Min, bb.Max) then
|
|
return false
|
|
end
|
|
|
|
if g.HoveredId ~= 0 and g.HoveredId ~= id and not g.HoveredIdAllowOverlap then
|
|
return false
|
|
end
|
|
|
|
if g.ActiveId ~= 0 and g.ActiveId ~= id and not g.ActiveIdAllowOverlap then
|
|
if not g.ActiveIdFromShortcut then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- We are done with rectangle culling so we can perform heavier checks now.
|
|
if (bit32.band(item_flags, ImGuiItemFlags.NoWindowHoverableCheck) == 0) and not ImGui.IsWindowContentHoverable(window, ImGuiHoveredFlags.None) then
|
|
g.HoveredIdIsDisabled = true
|
|
return false
|
|
end
|
|
|
|
-- We exceptionally allow this function to be called with id==0 to allow using it for easy high-level
|
|
-- hover test in widgets code. We could also decide to split this function is two.
|
|
if id ~= 0 then
|
|
-- Drag source doesn't report as hovered
|
|
if g.DragDropActive and g.DragDropPayload.SourceId == id and (bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.SourceNoDisableHover) == 0) then
|
|
return false
|
|
end
|
|
|
|
ImGui.SetHoveredID(id)
|
|
|
|
-- AllowOverlap mode (rarely used) requires previous frame HoveredId to be nil or to match.
|
|
-- This allows using patterns where a later submitted widget overlaps a previous one. Generally perceived as a front-to-back hit-test.
|
|
if bit32.band(item_flags, ImGuiItemFlags.AllowOverlap) ~= 0 then
|
|
g.HoveredIdAllowOverlap = true
|
|
if g.HoveredIdPreviousFrame ~= id then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Display shortcut (only works with mouse)
|
|
-- (ImGuiItemStatusFlags.HasShortcut in LastItemData denotes we want a tooltip)
|
|
if id == g.LastItemData.ID and (bit32.band(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HasShortcut) ~= 0) and g.ActiveId ~= id then
|
|
if ImGui.IsItemHovered(bit32.bor(ImGuiHoveredFlags.ForTooltip, ImGuiHoveredFlags.DelayNormal)) then
|
|
ImGui.SetTooltip("%s", ImGui.GetKeyChordName(g.LastItemData.Shortcut))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- When disabled we'll return false but still set HoveredId
|
|
if bit32.band(item_flags, ImGuiItemFlags.Disabled) ~= 0 then
|
|
-- Release active id if turning disabled
|
|
if g.ActiveId == id and id ~= 0 then
|
|
ImGui.ClearActiveID()
|
|
end
|
|
g.HoveredIdIsDisabled = true
|
|
return false
|
|
end
|
|
|
|
if g.NavHighlightItemUnderNav and (bit32.band(item_flags, ImGuiItemFlags.NoNavDisableMouseHover) == 0) then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- FIXME: This is inlined/duplicated in ItemAdd()
|
|
-- FIXME: The id ~= 0 path is not used by our codebase, may get rid of it?
|
|
--- @param bb ImRect
|
|
--- @param id ImGuiID
|
|
--- @return bool
|
|
function ImGui.IsClippedEx(bb, id)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if not bb:Overlaps(window.ClipRect) then
|
|
if id == 0 or (id ~= g.ActiveId and id ~= g.ActiveIdPreviousFrame and id ~= g.NavId and id ~= g.NavActivateId) then
|
|
if not g.ItemUnclipByLog then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- This is also inlined in ItemAdd()
|
|
-- Note: if ImGuiItemStatusFlags.HasDisplayRect is set, user needs to set g.LastItemData.DisplayRect.
|
|
--- @param item_id ImGuiID
|
|
--- @param item_flags ImGuiItemFlags
|
|
--- @param status_flags ImGuiItemStatusFlags
|
|
--- @param item_rect ImRect
|
|
function ImGui.SetLastItemData(item_id, item_flags, status_flags, item_rect)
|
|
local g = GImGui
|
|
g.LastItemData.ID = item_id
|
|
g.LastItemData.ItemFlags = item_flags
|
|
g.LastItemData.StatusFlags = status_flags
|
|
ImRect_Copy(g.LastItemData.Rect, item_rect)
|
|
ImRect_Copy(g.LastItemData.NavRect, item_rect)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param rect ImRect
|
|
function ImGui.SetLastItemDataForWindow(window, rect)
|
|
local g = GImGui
|
|
if window.DockIsActive then
|
|
ImGui.SetLastItemData(window.MoveId, g.CurrentItemFlags, window.DC.DockTabItemStatusFlags, window.DC.DockTabItemRect)
|
|
else
|
|
ImGui.SetLastItemData(window.MoveId, g.CurrentItemFlags, window.DC.WindowItemStatusFlags, rect)
|
|
end
|
|
end
|
|
|
|
-- Pass in translated ASCII characters for text input.
|
|
-- - with glfw you can get those from the callback set in glfwSetCharCallback()
|
|
-- - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message
|
|
-- FIXME: Should in theory be called "AddCharacterEvent()" to be consistent with new API
|
|
--- @param c unsigned_int
|
|
function MT.ImGuiIO:AddInputCharacter(c)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = self.Ctx
|
|
if c == 0 or not self.AppAcceptingEvents then
|
|
return
|
|
end
|
|
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.Text
|
|
e.Source = ImGuiInputSource.Keyboard
|
|
e.EventId = g.InputEventsNextEventId
|
|
g.InputEventsNextEventId = g.InputEventsNextEventId + 1
|
|
e.Text = ImGuiInputEventText()
|
|
e.Text.Char = c
|
|
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
-- UTF16 strings use surrogate pairs to encode codepoints >= 0x10000, so
|
|
-- we should save the high surrogate.
|
|
--- @param c ImWchar16
|
|
function MT.ImGuiIO:AddInputCharacterUTF16(c)
|
|
if (c == 0 and self.InputQueueSurrogate == 0) or not self.AppAcceptingEvents then
|
|
return
|
|
end
|
|
|
|
if bit32.band(c, 0xFC00) == 0xD800 then -- High surrogate, must save
|
|
if self.InputQueueSurrogate ~= 0 then
|
|
self:AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID)
|
|
end
|
|
self.InputQueueSurrogate = c
|
|
return
|
|
end
|
|
|
|
local cp = c
|
|
if self.InputQueueSurrogate ~= 0 then
|
|
if bit32.band(c, 0xFC00) ~= 0xDC00 then -- Invalid low surrogate
|
|
self:AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID)
|
|
else
|
|
-- #if IM_UNICODE_CODEPOINT_MAX == 0xFFFF
|
|
cp = IM_UNICODE_CODEPOINT_INVALID -- Codepoint will not fit in ImWchar
|
|
-- #endif
|
|
end
|
|
|
|
self.InputQueueSurrogate = 0
|
|
end
|
|
self:AddInputCharacter(cp)
|
|
end
|
|
|
|
-- Clear all incoming events.
|
|
function MT.ImGuiIO:ClearEventsQueue()
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = GImGui
|
|
g.InputEventsQueue:clear()
|
|
end
|
|
|
|
-- Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons.
|
|
function MT.ImGuiIO:ClearInputKeys()
|
|
local g = self.Ctx
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
if not ImGui.IsMouseKey(key) then
|
|
local key_data = g.IO.KeysData[key - ImGuiKey.NamedKey_BEGIN]
|
|
key_data.Down = false
|
|
key_data.DownDuration = -1.0
|
|
key_data.DownDurationPrev = -1.0
|
|
end
|
|
end
|
|
self.KeyCtrl = false
|
|
self.KeyShift = false
|
|
self.KeyAlt = false
|
|
self.KeySuper = false
|
|
self.KeyMods = ImGuiMod_None
|
|
self.InputQueueCharacters:resize(0) -- Behavior of old ClearInputCharacters().
|
|
end
|
|
|
|
function MT.ImGuiIO:ClearInputMouse()
|
|
for key = ImGuiKey_Mouse_BEGIN, ImGuiKey_Mouse_END - 1 do
|
|
local key_data = self.KeysData[key - ImGuiKey.NamedKey_BEGIN]
|
|
if key_data then
|
|
key_data.Down = false
|
|
key_data.DownDuration = -1.0
|
|
key_data.DownDurationPrev = -1.0
|
|
end
|
|
end
|
|
|
|
self.MousePos = ImVec2(-FLT_MAX, -FLT_MAX)
|
|
|
|
for n = 0, 2 do -- IM_COUNTOF(MouseDown)
|
|
self.MouseDown[n] = false
|
|
self.MouseDownDuration[n] = -1.0
|
|
self.MouseDownDurationPrev[n] = -1.0
|
|
end
|
|
|
|
self.MouseWheel = 0
|
|
self.MouseWheelH = 0
|
|
end
|
|
|
|
--- @param ctx ImGuiContext
|
|
--- @param type ImGuiInputEventType
|
|
--- @param arg? int
|
|
--- @return ImGuiInputEvent?
|
|
function MT.ImGuiIO:FindLatestInputEvent(ctx, type, arg)
|
|
if arg == nil then arg = -1 end
|
|
|
|
local g = ctx
|
|
for n = g.InputEventsQueue.Size, 1, -1 do
|
|
local e = g.InputEventsQueue.Data[n]
|
|
|
|
if e.Type ~= type then
|
|
elseif type == ImGuiInputEventType.Key and e.Key.Key ~= arg then
|
|
elseif type == ImGuiInputEventType.MouseButton and e.MouseButton.Button ~= arg then
|
|
else
|
|
return e
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- Queue a new key down/up event.
|
|
-- - ImGuiKey key: Translated key (as in, generally ImGuiKey.A matches the key end-user would use to emit an 'A' character)
|
|
-- - bool down: Is the key down? use false to signify a key release.
|
|
-- - float analog_value: 0.0f..1.0f
|
|
-- IMPORTANT: THIS FUNCTION AND OTHER "ADD" GRABS THE CONTEXT FROM OUR INSTANCE.
|
|
-- WE NEED TO ENSURE THAT ALL FUNCTION CALLS ARE FULFILLING THIS, WHICH IS WHY GetKeyData() HAS AN EXPLICIT CONTEXT.
|
|
--- @param key ImGuiKey
|
|
--- @param down bool
|
|
--- @param analog_value float
|
|
function MT.ImGuiIO:AddKeyAnalogEvent(key, down, analog_value)
|
|
-- if (e->Down) { IMGUI_DEBUG_LOG_IO("AddKeyEvent() Key='%s' %d, NativeKeycode = %d, NativeScancode = %d\n", ImGui::GetKeyName(e->Key), e->Down, e->NativeKeycode, e->NativeScancode); }
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
if key == ImGuiKey.None or not self.AppAcceptingEvents then
|
|
return
|
|
end
|
|
|
|
local g = self.Ctx
|
|
IM_ASSERT(ImGui.IsNamedKeyOrMod(key)) -- Backend needs to pass a valid ImGuiKey constant. 0..511 values are legacy native key codes which are not accepted by this API.
|
|
IM_ASSERT(ImGui.IsAliasKey(key) == false) -- Backend cannot submit ImGuiKey.MouseXXX values they are automatically inferred from AddMouseXXX() events.
|
|
|
|
-- MacOS: swap Cmd(Super) and Ctrl
|
|
if (g.IO.ConfigMacOSXBehaviors) then
|
|
if (key == ImGuiMod_Super) then key = ImGuiMod_Ctrl
|
|
elseif (key == ImGuiMod_Ctrl) then key = ImGuiMod_Super
|
|
elseif (key == ImGuiKey.LeftSuper) then key = ImGuiKey.LeftCtrl
|
|
elseif (key == ImGuiKey.RightSuper) then key = ImGuiKey.RightCtrl
|
|
elseif (key == ImGuiKey.LeftCtrl) then key = ImGuiKey.LeftSuper
|
|
elseif (key == ImGuiKey.RightCtrl) then key = ImGuiKey.RightSuper
|
|
end
|
|
end
|
|
|
|
-- Filter duplicate (in particular: key mods and gamepad analog values are commonly spammed)
|
|
local latest_event = self:FindLatestInputEvent(g, ImGuiInputEventType.Key, key)
|
|
local key_data = ImGui.GetKeyData(g, key)
|
|
local latest_key_down = latest_event and latest_event.Key.Down or key_data.Down
|
|
local latest_key_analog = latest_event and latest_event.Key.AnalogValue or key_data.AnalogValue
|
|
if latest_key_down == down and latest_key_analog == analog_value then
|
|
return
|
|
end
|
|
|
|
-- Add event
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.Key
|
|
e.Source = ImGui.IsGamepadKey(key) and ImGuiInputSource.Gamepad or ImGuiInputSource.Keyboard
|
|
e.EventId = g.InputEventsNextEventId
|
|
g.InputEventsNextEventId = g.InputEventsNextEventId + 1
|
|
e.Key = ImGuiInputEventKey()
|
|
e.Key.Key = key
|
|
e.Key.Down = down
|
|
e.Key.AnalogValue = analog_value
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
--- @param key ImGuiKey
|
|
--- @param down bool
|
|
function MT.ImGuiIO:AddKeyEvent(key, down)
|
|
if not self.AppAcceptingEvents then
|
|
return
|
|
end
|
|
self:AddKeyAnalogEvent(key, down, (down and 1.0 or 0.0))
|
|
end
|
|
|
|
-- Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen.
|
|
--- @param accepting_events bool
|
|
function MT.ImGuiIO:SetAppAcceptingEvents(accepting_events)
|
|
self.AppAcceptingEvents = accepting_events
|
|
end
|
|
|
|
-- This is not a real event, the data is latched in order to be stored in actual Mouse events.
|
|
-- This is so that duplicate events (e.g. Windows sending extraneous WM_MOUSEMOVE) gets filtered and are not leading to actual source changes.
|
|
--- @param source ImGuiMouseSource
|
|
function MT.ImGuiIO:AddMouseSourceEvent(source)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = self.Ctx
|
|
g.InputEventsNextMouseSource = source
|
|
end
|
|
|
|
--- @param viewport_id ImGuiID
|
|
function MT.ImGuiIO:AddMouseViewportEvent(viewport_id)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = self.Ctx
|
|
-- IM_ASSERT(g.IO.BackendFlags & ImGuiBackendFlags.HasMouseHoveredViewport)
|
|
if (not self.AppAcceptingEvents) then
|
|
return
|
|
end
|
|
|
|
-- Filter duplicate
|
|
local latest_event = self:FindLatestInputEvent(g, ImGuiInputEventType.MouseViewport)
|
|
local latest_viewport_id
|
|
if latest_event then
|
|
latest_viewport_id = latest_event.MouseViewport.HoveredViewportID
|
|
else
|
|
latest_viewport_id = g.IO.MouseHoveredViewport
|
|
end
|
|
if (latest_viewport_id == viewport_id) then
|
|
return
|
|
end
|
|
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.MouseViewport
|
|
e.Source = ImGuiInputSource.Mouse
|
|
e.MouseViewport = ImGuiInputEventMouseViewport()
|
|
e.MouseViewport.HoveredViewportID = viewport_id
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
--- @param focused bool
|
|
function MT.ImGuiIO:AddFocusEvent(focused)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = self.Ctx
|
|
|
|
-- Filter duplicate
|
|
local latest_event = self:FindLatestInputEvent(g, ImGuiInputEventType.Focus)
|
|
local latest_focused = latest_event and latest_event.AppFocused.Focused or (not g.IO.AppFocusLost)
|
|
if latest_focused == focused or (self.ConfigDebugIgnoreFocusLoss and not focused) then
|
|
return
|
|
end
|
|
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.Focus
|
|
e.EventId = g.InputEventsNextEventId
|
|
g.InputEventsNextEventId = g.InputEventsNextEventId + 1
|
|
e.AppFocused = ImGuiInputEventAppFocused()
|
|
e.AppFocused.Focused = focused
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
-- Queue a mouse move event
|
|
--- @param x float
|
|
--- @param y float
|
|
function MT.ImGuiIO:AddMousePosEvent(x, y)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
if not self.AppAcceptingEvents then
|
|
return
|
|
end
|
|
|
|
local g = self.Ctx
|
|
|
|
-- Apply same flooring as UpdateMouseInputs()
|
|
local x_val = x
|
|
if x > -FLT_MAX then
|
|
x_val = ImFloor(x)
|
|
end
|
|
local y_val = y
|
|
if y > -FLT_MAX then
|
|
y_val = ImFloor(y)
|
|
end
|
|
|
|
-- Filter duplicate
|
|
local latest_event = self:FindLatestInputEvent(g, ImGuiInputEventType.MousePos)
|
|
local latest_pos = latest_event and ImVec2(latest_event.MousePos.PosX, latest_event.MousePos.PosY) or self.MousePos
|
|
if latest_pos.x == x_val and latest_pos.y == y_val then
|
|
return
|
|
end
|
|
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.MousePos
|
|
e.Source = ImGuiInputSource.Mouse
|
|
e.EventId = g.InputEventsNextEventId
|
|
g.InputEventsNextEventId = g.InputEventsNextEventId + 1
|
|
e.MousePos = ImGuiInputEventMousePos()
|
|
e.MousePos.PosX = x_val
|
|
e.MousePos.PosY = y_val
|
|
e.MousePos.MouseSource = g.InputEventsNextMouseSource
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
--- @param mouse_button ImGuiMouseButton
|
|
--- @param down bool
|
|
function MT.ImGuiIO:AddMouseButtonEvent(mouse_button, down)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = self.Ctx
|
|
IM_ASSERT(mouse_button >= 0 and mouse_button < ImGuiMouseButton.COUNT)
|
|
if not self.AppAcceptingEvents then
|
|
return
|
|
end
|
|
|
|
-- On MacOS X: Convert Ctrl(Super)+Left click into Right-click: handle held button.
|
|
if self.ConfigMacOSXBehaviors and mouse_button == 0 and g.IO.MouseCtrlLeftAsRightClick then
|
|
-- Order of both statements matters: this event will still release mouse button 1
|
|
mouse_button = 1
|
|
if not down then
|
|
self.MouseCtrlLeftAsRightClick = false
|
|
end
|
|
end
|
|
|
|
-- Filter duplicate
|
|
local latest_event = self:FindLatestInputEvent(g, ImGuiInputEventType.MouseButton, mouse_button)
|
|
local latest_button_down = latest_event and latest_event.MouseButton.Down or self.MouseDown[mouse_button]
|
|
if latest_button_down == down then
|
|
return
|
|
end
|
|
|
|
-- On MacOS X: Convert Ctrl(Super)+Left click into Right-click.
|
|
-- - Note that this is actual physical Ctrl which is ImGuiMod_Super for us.
|
|
-- - At this point we want from !down to down, so this is handling the initial press.
|
|
if self.ConfigMacOSXBehaviors and mouse_button == 0 and down then
|
|
local latest_super_event = self:FindLatestInputEvent(g, ImGuiInputEventType.Key, ImGuiMod_Super)
|
|
if latest_super_event and latest_super_event.Key.Down or self.KeySuper then
|
|
-- IMGUI_DEBUG_LOG_IO("[io] Super+Left Click aliased into Right Click\n")
|
|
self.MouseCtrlLeftAsRightClick = true
|
|
self:AddMouseButtonEvent(1, true) -- This is just quicker to write that passing through, as we need to filter duplicate again.
|
|
return
|
|
end
|
|
end
|
|
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.MouseButton
|
|
e.Source = ImGuiInputSource.Mouse
|
|
e.EventId = g.InputEventsNextEventId
|
|
g.InputEventsNextEventId = g.InputEventsNextEventId + 1
|
|
e.MouseButton = ImGuiInputEventMouseButton()
|
|
e.MouseButton.Button = mouse_button
|
|
e.MouseButton.Down = down
|
|
e.MouseButton.MouseSource = g.InputEventsNextMouseSource
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
-- Queue a mouse wheel event (some mouse/API may only have a Y component)
|
|
--- @param wheel_x float
|
|
--- @param wheel_y float
|
|
function MT.ImGuiIO:AddMouseWheelEvent(wheel_x, wheel_y)
|
|
IM_ASSERT(self.Ctx ~= nil)
|
|
local g = self.Ctx
|
|
-- Filter duplicate (unlike most events, wheel values are relative and easy to filter)
|
|
if not self.AppAcceptingEvents or (wheel_x == 0 and wheel_y == 0) then
|
|
return
|
|
end
|
|
|
|
local e = ImGuiInputEvent()
|
|
e.Type = ImGuiInputEventType.MouseWheel
|
|
e.Source = ImGuiInputSource.Mouse
|
|
e.EventId = g.InputEventsNextEventId
|
|
g.InputEventsNextEventId = g.InputEventsNextEventId + 1
|
|
e.MouseWheel = ImGuiInputEventMouseWheel()
|
|
e.MouseWheel.WheelX = wheel_x
|
|
e.MouseWheel.WheelY = wheel_y
|
|
e.MouseWheel.MouseSource = g.InputEventsNextMouseSource
|
|
g.InputEventsQueue:push_back(e)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] INPUTS
|
|
-------------------------------------------------------------------------------
|
|
-- - GetModForLRModKey() [Internal]
|
|
-- - FixupKeyChord() [Internal]
|
|
-- - GetKeyData() [Internal]
|
|
-- - GetKeyIndex() [Internal]
|
|
-- - GetKeyName()
|
|
-- - GetKeyChordName() [Internal]
|
|
-- - CalcTypematicRepeatAmount() [Internal]
|
|
-- - GetTypematicRepeatRate() [Internal]
|
|
-- - GetKeyPressedAmount() [Internal]
|
|
-- - GetKeyMagnitude2d() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
-- - UpdateKeyRoutingTable() [Internal]
|
|
-- - GetRoutingIdFromOwnerId() [Internal]
|
|
-- - GetShortcutRoutingData() [Internal]
|
|
-- - CalcRoutingScore() [Internal]
|
|
-- - SetShortcutRouting() [Internal]
|
|
-- - TestShortcutRouting() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
-- - IsKeyDown()
|
|
-- - IsKeyPressed()
|
|
-- - IsKeyReleased()
|
|
-------------------------------------------------------------------------------
|
|
-- - IsMouseDown()
|
|
-- - IsMouseClicked()
|
|
-- - IsMouseReleased()
|
|
-- - IsMouseDoubleClicked()
|
|
-- - GetMouseClickedCount()
|
|
-- - GetItemClickedCountWithSingleClickDelay()
|
|
-- - IsMouseHoveringRect() [Internal]
|
|
-- - IsMouseDragPastThreshold() [Internal]
|
|
-- - IsMouseDragging()
|
|
-- - GetMousePos()
|
|
-- - SetMousePos() [Internal]
|
|
-- - GetMousePosOnOpeningCurrentPopup()
|
|
-- - IsMousePosValid()
|
|
-- - IsAnyMouseDown()
|
|
-- - GetMouseDragDelta()
|
|
-- - ResetMouseDragDelta()
|
|
-- - GetMouseCursor()
|
|
-- - SetMouseCursor()
|
|
-------------------------------------------------------------------------------
|
|
-- - UpdateAliasKey()
|
|
-- - GetMergedModsFromKeys()
|
|
-- - UpdateKeyboardInputs()
|
|
-- - UpdateMouseInputs()
|
|
-------------------------------------------------------------------------------
|
|
-- - LockWheelingWindow [Internal]
|
|
-- - FindBestWheelingWindow [Internal]
|
|
-- - UpdateMouseWheel() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
-- - SetNextFrameWantCaptureKeyboard()
|
|
-- - SetNextFrameWantCaptureMouse()
|
|
-------------------------------------------------------------------------------
|
|
-- - GetInputSourceName() [Internal]
|
|
-- - DebugPrintInputEvent() [Internal]
|
|
-- - UpdateInputEvents() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
-- - GetKeyOwner() [Internal]
|
|
-- - TestKeyOwner() [Internal]
|
|
-- - SetKeyOwner() [Internal]
|
|
-- - SetItemKeyOwner() [Internal]
|
|
-- - Shortcut() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param key ImGuiKey
|
|
local function GetModForLRModKey(key)
|
|
if key == ImGuiKey.LeftCtrl or key == ImGuiKey.RightCtrl then
|
|
return ImGuiMod_Ctrl
|
|
end
|
|
if key == ImGuiKey.LeftShift or key == ImGuiKey.RightShift then
|
|
return ImGuiMod_Shift
|
|
end
|
|
if key == ImGuiKey.LeftAlt or key == ImGuiKey.RightAlt then
|
|
return ImGuiMod_Alt
|
|
end
|
|
if key == ImGuiKey.LeftSuper or key == ImGuiKey.RightSuper then
|
|
return ImGuiMod_Super
|
|
end
|
|
return ImGuiMod_None
|
|
end
|
|
|
|
--- @param key_chord ImGuiKeyChord
|
|
function ImGui.FixupKeyChord(key_chord)
|
|
-- Add ImGuiMod_XXXX when a corresponding ImGuiKey.LeftXXX/ImGuiKey.RightXXX is specified.
|
|
local key = bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_))
|
|
if ImGui.IsLRModKey(key) then
|
|
key_chord = bit32.bor(key_chord, GetModForLRModKey(key))
|
|
end
|
|
return key_chord
|
|
end
|
|
|
|
--- @param ctx? ImGuiContext
|
|
--- @param key ImGuiKey
|
|
--- @return ImGuiKeyData, int
|
|
function ImGui.GetKeyData(ctx, key)
|
|
if ctx == nil then ctx = GImGui end
|
|
|
|
-- Special storage location for mods
|
|
if bit32.band(key, ImGuiMod_Mask_) ~= 0 then
|
|
key = ImGui.ConvertSingleModFlagToKey(key)
|
|
end
|
|
|
|
IM_ASSERT(ImGui.IsNamedKey(key), "Support for user key indices was dropped in favor of ImGuiKey. Please update backend & user code.")
|
|
local idx = key - ImGuiKey.NamedKey_BEGIN
|
|
return ctx.IO.KeysData[idx], idx
|
|
end
|
|
|
|
-- Return 2D vector representing the combination of four cardinal direction, with analog value support (for e.g. ImGuiKey.GamepadLStick* values).
|
|
--- @param key_left ImGuiKey
|
|
--- @param key_right ImGuiKey
|
|
--- @param key_up ImGuiKey
|
|
--- @param key_down ImGuiKey
|
|
function ImGui.GetKeyMagnitude2d(key_left, key_right, key_up, key_down)
|
|
return ImVec2(
|
|
ImGui.GetKeyData(nil, key_right).AnalogValue - ImGui.GetKeyData(nil, key_left).AnalogValue,
|
|
ImGui.GetKeyData(nil, key_down).AnalogValue - ImGui.GetKeyData(nil, key_up).AnalogValue
|
|
)
|
|
end
|
|
|
|
-- TestKeyOwner(..., ID) : (owner == None || owner == ID)
|
|
-- TestKeyOwner(..., None) : (owner == None)
|
|
-- TestKeyOwner(..., Any) : no owner test
|
|
-- All paths are also testing for key not being locked, for the rare cases that key have been locked with using ImGuiInputFlags.LockXXX flags.
|
|
--- @param key ImGuiKey
|
|
--- @param owner_id ImGuiID
|
|
function ImGui.TestKeyOwner(key, owner_id)
|
|
if not ImGui.IsNamedKeyOrMod(key) then
|
|
return true
|
|
end
|
|
|
|
local g = GImGui --- @cast g ImGuiContext
|
|
if g.ActiveIdUsingAllKeyboardKeys and owner_id ~= g.ActiveId and owner_id ~= ImGuiKeyOwner_Any then
|
|
if key >= ImGuiKey_Keyboard_BEGIN and key < ImGuiKey_Keyboard_END then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local owner_data = ImGui.GetKeyOwnerData(g, key)
|
|
if owner_id == ImGuiKeyOwner_Any then
|
|
return not owner_data.LockThisFrame
|
|
end
|
|
|
|
-- Note: SetKeyOwner() sets OwnerCurr. It is not strictly required for most mouse routing overlap (because of ActiveId/HoveredId
|
|
-- are acting as filter before this has a chance to filter), but sane as soon as user tries to look into things.
|
|
-- Setting OwnerCurr in SetKeyOwner() is more consistent than testing OwnerNext here: would be inconsistent with getter and other functions.
|
|
if owner_data.OwnerCurr ~= owner_id then
|
|
if owner_data.LockThisFrame then
|
|
return false
|
|
end
|
|
if owner_data.OwnerCurr ~= ImGuiKeyOwner_NoOwner then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Note that Dear ImGui doesn't know the meaning/semantic of ImGuiKey from 0..511: they are legacy native keycodes.
|
|
-- Consider transitioning from 'IsKeyDown(MY_ENGINE_KEY_A)' (<1.87) to IsKeyDown(ImGuiKey.A) (>= 1.87)
|
|
--- @param key ImGuiKey
|
|
--- @param owner_id? ImGuiID
|
|
function ImGui.IsKeyDown(key, owner_id)
|
|
if owner_id == nil then owner_id = ImGuiKeyOwner_Any end
|
|
|
|
local key_data = ImGui.GetKeyData(nil, key)
|
|
if not key_data.Down then
|
|
return false
|
|
end
|
|
if not ImGui.TestKeyOwner(key, owner_id) then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- @param key ImGuiKey
|
|
--- @param is_repeat? bool
|
|
function ImGui.IsKeyPressed(key, is_repeat)
|
|
if is_repeat == nil then is_repeat = true end
|
|
|
|
return ImGui.IsKeyPressedEx(key, is_repeat and ImGuiInputFlags.Repeat or ImGuiInputFlags.None, ImGuiKeyOwner_Any)
|
|
end
|
|
|
|
--- @param key ImGuiKey
|
|
--- @param owner_id? ImGuiID
|
|
--- @return bool
|
|
function ImGui.IsKeyReleased(key, owner_id)
|
|
if owner_id == nil then owner_id = ImGuiKeyOwner_Any end
|
|
|
|
local key_data = ImGui.GetKeyData(nil, key)
|
|
if key_data.DownDurationPrev < 0.0 or key_data.Down then
|
|
return false
|
|
end
|
|
if not ImGui.TestKeyOwner(key, owner_id) then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Important: unlike legacy IsKeyPressed(ImGuiKey, bool repeat=true) which DEFAULT to repeat, this requires EXPLICIT repeat.
|
|
--- @param key ImGuiKey
|
|
--- @param flags ImGuiInputFlags
|
|
--- @param owner_id? ImGuiID
|
|
function ImGui.IsKeyPressedEx(key, flags, owner_id)
|
|
if owner_id == nil then owner_id = 0 end
|
|
|
|
local g = GImGui
|
|
local key_data = ImGui.GetKeyData(g, key)
|
|
|
|
if not key_data.Down then -- In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership)
|
|
return false
|
|
end
|
|
local t = key_data.DownDuration
|
|
if t < 0.0 then
|
|
return false
|
|
end
|
|
IM_ASSERT(bit32.band(flags, bit32.bnot(ImGuiInputFlags.SupportedByIsKeyPressed)) == 0) -- Passing flags not supported by this function!
|
|
if bit32.band(flags, bit32.bor(ImGuiInputFlags.RepeatRateMask_, ImGuiInputFlags.RepeatUntilMask_)) ~= 0 then -- Setting any _RepeatXXX option enables _Repeat
|
|
flags = bit32.bor(flags, ImGuiInputFlags.Repeat)
|
|
end
|
|
|
|
local pressed = (t == 0.0)
|
|
if not pressed and bit32.band(flags, ImGuiInputFlags.Repeat) ~= 0 then
|
|
local repeat_delay, repeat_rate = ImGui.GetTypematicRepeatRate(flags)
|
|
pressed = (t > repeat_delay) and (ImGui.GetKeyPressedAmount(key, repeat_delay, repeat_rate) > 0)
|
|
if pressed and bit32.band(flags, ImGuiInputFlags.RepeatUntilMask_) ~= 0 then
|
|
-- Slightly bias 'key_pressed_time' as DownDuration is an accumulation of DeltaTime which we compare to an absolute time value.
|
|
-- Ideally we'd replace DownDuration with KeyPressedTime but it would break user's code.
|
|
local key_pressed_time = g.Time - t + 0.00001
|
|
if bit32.band(flags, ImGuiInputFlags.RepeatUntilKeyModsChange) ~= 0 and g.LastKeyModsChangeTime > key_pressed_time then
|
|
pressed = false
|
|
end
|
|
if bit32.band(flags, ImGuiInputFlags.RepeatUntilKeyModsChangeFromNone) ~= 0 and g.LastKeyModsChangeFromNoneTime > key_pressed_time then
|
|
pressed = false
|
|
end
|
|
if bit32.band(flags, ImGuiInputFlags.RepeatUntilOtherKeyPress) ~= 0 and g.LastKeyboardKeyPressTime > key_pressed_time then
|
|
pressed = false
|
|
end
|
|
end
|
|
end
|
|
|
|
if not pressed then
|
|
return false
|
|
end
|
|
if not ImGui.TestKeyOwner(key, owner_id) then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- TODO:
|
|
-- Return untranslated names: on macOS, Cmd key will show as Ctrl, Ctrl key will show as super.
|
|
function ImGui.GetKeyChordName(key_chord)
|
|
local key = bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_))
|
|
if ImGui.IsLRModKey(key) then
|
|
key_chord = bit32.band(key_chord, bit32.bnot(GetModForLRModKey(key))) -- Return "Ctrl+LeftShift" instead of "Ctrl+Shift+LeftShift"
|
|
end
|
|
local s = ((bit32.band(key_chord, ImGuiMod_Ctrl) ~= 0) and "Ctrl+" or "")
|
|
.. ((bit32.band(key_chord, ImGuiMod_Shift) ~= 0) and "Shift+" or "")
|
|
.. ((bit32.band(key_chord, ImGuiMod_Alt) ~= 0) and "Alt+" or "")
|
|
.. ((bit32.band(key_chord, ImGuiMod_Super) ~= 0) and "Super+" or "")
|
|
.. ((key ~= ImGuiKey.None or key_chord == ImGuiKey.None) and ImGui.GetKeyName(key) or "")
|
|
if key == ImGuiKey.None and key_chord ~= 0 and #s > 0 then
|
|
s = string.sub(s, 1, #s - 1) -- Remove trailing '+'
|
|
end
|
|
return s
|
|
end
|
|
|
|
-- Those names are provided for debugging purpose and are not meant to be saved persistently nor compared.
|
|
local GKeyNames = {
|
|
"Tab", "LeftArrow", "RightArrow", "UpArrow", "DownArrow", "PageUp", "PageDown",
|
|
"Home", "End", "Insert", "Delete", "Backspace", "Space", "Enter", "Escape",
|
|
"LeftCtrl", "LeftShift", "LeftAlt", "LeftSuper", "RightCtrl", "RightShift", "RightAlt", "RightSuper", "Menu",
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
|
|
"I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
|
"F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12",
|
|
"F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24",
|
|
"Apostrophe", "Comma", "Minus", "Period", "Slash", "Semicolon", "Equal", "LeftBracket",
|
|
"Backslash", "RightBracket", "GraveAccent", "CapsLock", "ScrollLock", "NumLock", "PrintScreen",
|
|
"Pause", "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", "Keypad5", "Keypad6",
|
|
"Keypad7", "Keypad8", "Keypad9", "KeypadDecimal", "KeypadDivide", "KeypadMultiply",
|
|
"KeypadSubtract", "KeypadAdd", "KeypadEnter", "KeypadEqual",
|
|
"AppBack", "AppForward", "Oem102",
|
|
"GamepadStart", "GamepadBack",
|
|
"GamepadFaceLeft", "GamepadFaceRight", "GamepadFaceUp", "GamepadFaceDown",
|
|
"GamepadDpadLeft", "GamepadDpadRight", "GamepadDpadUp", "GamepadDpadDown",
|
|
"GamepadL1", "GamepadR1", "GamepadL2", "GamepadR2", "GamepadL3", "GamepadR3",
|
|
"GamepadLStickLeft", "GamepadLStickRight", "GamepadLStickUp", "GamepadLStickDown",
|
|
"GamepadRStickLeft", "GamepadRStickRight", "GamepadRStickUp", "GamepadRStickDown",
|
|
"MouseLeft", "MouseRight", "MouseMiddle", "MouseX1", "MouseX2", "MouseWheelX", "MouseWheelY",
|
|
"ModCtrl", "ModShift", "ModAlt", "ModSuper", -- ReservedForModXXX are showing the ModXXX names.
|
|
}
|
|
|
|
--- @param key ImGuiKey
|
|
--- @return string
|
|
function ImGui.GetKeyName(key)
|
|
if key == ImGuiKey.None then
|
|
return "None"
|
|
end
|
|
IM_ASSERT(ImGui.IsNamedKeyOrMod(key), "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code.")
|
|
if bit32.band(key, ImGuiMod_Mask_) ~= 0 then
|
|
key = ImGui.ConvertSingleModFlagToKey(key)
|
|
end
|
|
if not ImGui.IsNamedKey(key) then
|
|
return "Unknown"
|
|
end
|
|
return GKeyNames[key - ImGuiKey.NamedKey_BEGIN + 1] or "Unknown"
|
|
end
|
|
|
|
--- @param key ImGuiKey
|
|
--- @return ImGuiID
|
|
function ImGui.GetKeyOwner(key)
|
|
if not ImGui.IsNamedKeyOrMod(key) then
|
|
return ImGuiKeyOwner_NoOwner
|
|
end
|
|
|
|
local g = GImGui
|
|
local owner_data = ImGui.GetKeyOwnerData(g, key)
|
|
local owner_id = owner_data.OwnerCurr
|
|
|
|
if g.ActiveIdUsingAllKeyboardKeys and owner_id ~= g.ActiveId and owner_id ~= ImGuiKeyOwner_Any then
|
|
if key >= ImGuiKey_Keyboard_BEGIN and key < ImGuiKey_Keyboard_END then
|
|
return ImGuiKeyOwner_NoOwner
|
|
end
|
|
end
|
|
|
|
return owner_id
|
|
end
|
|
|
|
-- t0 = previous time (e.g.: g.Time - g.IO.DeltaTime)
|
|
-- t1 = current time (e.g.: g.Time)
|
|
-- An event is triggered at:
|
|
-- t = 0.0f t = repeat_delay, t = repeat_delay + repeat_rate*N
|
|
--- @param t0 float
|
|
--- @param t1 float
|
|
--- @param repeat_delay float
|
|
--- @param repeat_rate float
|
|
function ImGui.CalcTypematicRepeatAmount(t0, t1, repeat_delay, repeat_rate)
|
|
if t1 == 0.0 then return 1 end
|
|
if t0 >= t1 then return 0 end
|
|
if repeat_rate <= 0.0 then
|
|
if t0 < repeat_delay and t1 >= repeat_delay then
|
|
return 1
|
|
else
|
|
return 0
|
|
end
|
|
end
|
|
|
|
local count_t0
|
|
if t0 < repeat_delay then
|
|
count_t0 = -1
|
|
else
|
|
count_t0 = math.floor((t0 - repeat_delay) / repeat_rate)
|
|
end
|
|
|
|
local count_t1
|
|
if t1 < repeat_delay then
|
|
count_t1 = -1
|
|
else
|
|
count_t1 = math.floor((t1 - repeat_delay) / repeat_rate)
|
|
end
|
|
|
|
return count_t1 - count_t0
|
|
end
|
|
|
|
--- @param flags ImGuiInputFlags
|
|
--- @return float repeat_delay
|
|
--- @return float repeat_rate
|
|
function ImGui.GetTypematicRepeatRate(flags)
|
|
local g = GImGui
|
|
flags = bit32.band(flags, ImGuiInputFlags.RepeatRateMask_)
|
|
if flags == ImGuiInputFlags.RepeatRateNavMove then repeat_delay = g.IO.KeyRepeatDelay * 0.72; repeat_rate = g.IO.KeyRepeatRate * 0.80; return repeat_delay, repeat_rate
|
|
elseif flags == ImGuiInputFlags.RepeatRateNavTweak then repeat_delay = g.IO.KeyRepeatDelay * 0.72; repeat_rate = g.IO.KeyRepeatRate * 0.30; return repeat_delay, repeat_rate
|
|
else repeat_delay = g.IO.KeyRepeatDelay * 1.00; repeat_rate = g.IO.KeyRepeatRate * 1.00; return repeat_delay, repeat_rate end
|
|
end
|
|
|
|
-- Return value representing the number of presses in the last time period, for the given repeat rate
|
|
-- (most often returns 0 or 1. The result is generally only >1 when RepeatRate is smaller than DeltaTime, aka large DeltaTime or fast RepeatRate)
|
|
--- @param key ImGuiKey
|
|
--- @param repeat_delay float
|
|
--- @param repeat_rate float
|
|
function ImGui.GetKeyPressedAmount(key, repeat_delay, repeat_rate)
|
|
local g = GImGui
|
|
local key_data = ImGui.GetKeyData(g, key)
|
|
if not key_data.Down then -- In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership)
|
|
return 0
|
|
end
|
|
local t = key_data.DownDuration
|
|
return ImGui.CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, repeat_delay, repeat_rate)
|
|
end
|
|
|
|
-- Rewrite routing data buffers to strip old entries + sort by key to make queries not touch scattered data.
|
|
-- Entries D,A,B,B,A,C,B --> A,A,B,B,B,C,D
|
|
-- Index A:1 B:2 C:5 D:0 --> A:0 B:2 C:5 D:6
|
|
-- See 'Metrics->Key Owners & Shortcut Routing' to visualize the result of that operation.
|
|
--- @param rt ImGuiKeyRoutingTable
|
|
function ImGui.UpdateKeyRoutingTable(rt)
|
|
local g = GImGui --[[@as ImGuiContext]]
|
|
rt.EntriesNext:resize(0)
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
local new_routing_start_idx = rt.EntriesNext.Size + 1
|
|
local routing_entry
|
|
local old_routing_idx = rt.Index[key - ImGuiKey.NamedKey_BEGIN + 1]
|
|
while old_routing_idx ~= -1 do
|
|
repeat
|
|
routing_entry = rt.Entries[old_routing_idx]
|
|
routing_entry.RoutingCurrScore = routing_entry.RoutingNextScore
|
|
routing_entry.RoutingCurr = routing_entry.RoutingNext -- Update entry
|
|
routing_entry.RoutingNext = ImGuiKeyOwner_NoOwner
|
|
routing_entry.RoutingNextScore = 0
|
|
if routing_entry.RoutingCurr == ImGuiKeyOwner_NoOwner then
|
|
break
|
|
end
|
|
|
|
rt.EntriesNext:push_back(routing_entry) -- Write alive ones into new buffer
|
|
|
|
-- Apply routing to owner if there's no owner already (RoutingCurr == None at this point)
|
|
-- This is the result of previous frame's SetShortcutRouting() call.
|
|
if routing_entry.Mods == g.IO.KeyMods then
|
|
local owner_data = ImGui.GetKeyOwnerData(g, key)
|
|
if owner_data.OwnerCurr == ImGuiKeyOwner_NoOwner then
|
|
owner_data.OwnerCurr = routing_entry.RoutingCurr
|
|
-- IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X) via Routing\n", GetKeyName(key), routing_entry->RoutingCurr);
|
|
end
|
|
end
|
|
|
|
until true
|
|
|
|
old_routing_idx = routing_entry.NextEntryIndex
|
|
end
|
|
|
|
-- Rewrite linked-list
|
|
rt.Index[key - ImGuiKey.NamedKey_BEGIN + 1] = (new_routing_start_idx <= rt.EntriesNext.Size and new_routing_start_idx or -1)
|
|
for n = new_routing_start_idx, rt.EntriesNext.Size do
|
|
rt.EntriesNext[n].NextEntryIndex = (n + 1 <= rt.EntriesNext.Size) and (n + 1) or -1
|
|
end
|
|
end
|
|
rt.Entries:swap(rt.EntriesNext) -- Swap new and old indexes
|
|
end
|
|
|
|
-- owner_id may be None/Any, but routing_id needs to be always be set, so we default to GetCurrentFocusScope().
|
|
--- @param owner_id ImGuiID
|
|
local function GetRoutingIdFromOwnerId(owner_id)
|
|
local g = GImGui
|
|
return (owner_id ~= ImGuiKeyOwner_NoOwner and owner_id ~= ImGuiKeyOwner_Any) and owner_id or g.CurrentFocusScopeId
|
|
end
|
|
|
|
--- @param key_chord ImGuiKeyChord
|
|
--- @return ImGuiKeyRoutingData
|
|
function ImGui.GetShortcutRoutingData(key_chord)
|
|
-- Majority of shortcuts will be Key + any number of Mods
|
|
-- We accept _Single_ mod with ImGuiKey.None.
|
|
-- - Shortcut(ImGuiKey.S | ImGuiMod_Ctrl) -- Legal
|
|
-- - Shortcut(ImGuiKey.S | ImGuiMod_Ctrl | ImGuiMod_Shift) -- Legal
|
|
-- - Shortcut(ImGuiMod_Ctrl) -- Legal
|
|
-- - Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift) -- Not legal
|
|
local g = GImGui
|
|
local rt = g.KeysRoutingTable
|
|
local key = bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_))
|
|
local mods = bit32.band(key_chord, ImGuiMod_Mask_)
|
|
if key == ImGuiKey.None then
|
|
key = ImGui.ConvertSingleModFlagToKey(mods)
|
|
end
|
|
IM_ASSERT(ImGui.IsNamedKey(key))
|
|
|
|
-- Get (in the majority of case, the linked list will have one element so this should be 2 reads.
|
|
-- Subsequent elements will be contiguous in memory as list is sorted/rebuilt in NewFrame).
|
|
local routing_data
|
|
local idx = rt.Index[key - ImGuiKey.NamedKey_BEGIN + 1]
|
|
while idx ~= -1 do
|
|
routing_data = rt.Entries.Data[idx]
|
|
if routing_data.Mods == mods then
|
|
return routing_data
|
|
end
|
|
|
|
idx = routing_data.NextEntryIndex
|
|
end
|
|
|
|
-- Add to linked-list
|
|
local routing_data_idx = rt.Entries.Size + 1
|
|
rt.Entries:push_back(ImGuiKeyRoutingData())
|
|
routing_data = rt.Entries[routing_data_idx]
|
|
routing_data.Mods = mods
|
|
routing_data.NextEntryIndex = rt.Index[key - ImGuiKey.NamedKey_BEGIN + 1] -- Setup linked list
|
|
rt.Index[key - ImGuiKey.NamedKey_BEGIN + 1] = routing_data_idx
|
|
|
|
return routing_data
|
|
end
|
|
|
|
-- Current score encoding
|
|
-- - 0: Never route
|
|
-- - 1: ImGuiInputFlags.RouteGlobal (lower priority)
|
|
-- - 100..199: ImGuiInputFlags.RouteFocused (if window in focus-stack)
|
|
-- 200: ImGuiInputFlags.RouteGlobal | ImGuiInputFlags.RouteOverFocused
|
|
-- 300: ImGuiInputFlags.RouteActive or ImGuiInputFlags.RouteFocused (if item active)
|
|
-- 400: ImGuiInputFlags.RouteGlobal | ImGuiInputFlags.RouteOverActive
|
|
-- - 500..599: ImGuiInputFlags.RouteFocused | ImGuiInputFlags.RouteOverActive (if window in focus-stack) (higher priority)
|
|
-- 'flags' should include an explicit routing policy
|
|
--- @param focus_scope_id ImGuiID
|
|
--- @param owner_id ImGuiID
|
|
--- @param flags ImGuiInputFlags
|
|
local function CalcRoutingScore(focus_scope_id, owner_id, flags)
|
|
local g = GImGui
|
|
if bit32.band(flags, ImGuiInputFlags.RouteFocused) ~= 0 then
|
|
-- ActiveID gets high priority
|
|
-- (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it)
|
|
if owner_id ~= 0 and g.ActiveId == owner_id then
|
|
return 300
|
|
end
|
|
|
|
-- Score based on distance to focused window (lower is better)
|
|
-- Assuming both windows are submitting a routing request,
|
|
-- - When Window....... is focused -> Window scores 3 (best), Window/ChildB scores 255 (no match)
|
|
-- - When Window/ChildB is focused -> Window scores 4, Window/ChildB scores 3 (best)
|
|
-- Assuming only WindowA is submitting a routing request,
|
|
-- - When Window/ChildB is focused -> Window scores 4 (best), Window/ChildB doesn't have a score.
|
|
-- This essentially follow the window.ParentWindowForFocusRoute chain.
|
|
if focus_scope_id == 0 then
|
|
return 0
|
|
end
|
|
for index_in_focus_path = 1, g.NavFocusRoute.Size do
|
|
if g.NavFocusRoute.Data[index_in_focus_path].ID == focus_scope_id then
|
|
if bit32.band(flags, ImGuiInputFlags.RouteOverActive) ~= 0 then -- && g.ActiveId != 0 && g.ActiveId != owner_id)
|
|
return 599 - (index_in_focus_path - 1)
|
|
else
|
|
return 199 - (index_in_focus_path - 1)
|
|
end
|
|
end
|
|
end
|
|
return 0
|
|
elseif bit32.band(flags, ImGuiInputFlags.RouteActive) ~= 0 then
|
|
if owner_id ~= 0 and g.ActiveId == owner_id then
|
|
return 300
|
|
end
|
|
return 0
|
|
elseif bit32.band(flags, ImGuiInputFlags.RouteGlobal) ~= 0 then
|
|
if bit32.band(flags, ImGuiInputFlags.RouteOverActive) ~= 0 then
|
|
return 400
|
|
end
|
|
if owner_id ~= 0 and g.ActiveId == owner_id then
|
|
return 300
|
|
end
|
|
if bit32.band(flags, ImGuiInputFlags.RouteOverFocused) ~= 0 then
|
|
return 200
|
|
end
|
|
return 1
|
|
end
|
|
IM_ASSERT(false)
|
|
return 0
|
|
end
|
|
|
|
-- - We need this to filter some Shortcut() routes when an item e.g. an InputText() is active
|
|
-- e.g. ImGuiKey.G won't be considered a shortcut when item is active, but ImGuiMod|ImGuiKey.G can be.
|
|
-- - This is also used by UpdateInputEvents() to avoid trickling in the most common case of e.g. pressing ImGuiKey.G also emitting a G character.
|
|
--- @param key_chord ImGuiKeyChord
|
|
local function IsKeyChordPotentiallyCharInput(key_chord)
|
|
-- Mimic 'ignore_char_inputs' logic in InputText()
|
|
local g = GImGui
|
|
|
|
-- When the right mods are pressed it cannot be a char input so we won't filter the shortcut out.
|
|
local mods = bit32.band(key_chord, ImGuiMod_Mask_)
|
|
local ignore_char_inputs = ((bit32.band(mods, ImGuiMod_Ctrl) ~= 0) and (bit32.band(mods, ImGuiMod_Alt) == 0)) or (g.IO.ConfigMacOSXBehaviors and (bit32.band(mods, ImGuiMod_Ctrl) ~= 0))
|
|
if ignore_char_inputs then
|
|
return false
|
|
end
|
|
|
|
-- Return true for A-Z, 0-9 and other keys associated to char inputs. Other keys such as F1-F12 won't be filtered.
|
|
local key = bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_))
|
|
if key == ImGuiKey.None then
|
|
return false
|
|
end
|
|
return g.KeysMayBeCharInput:TestBit(key + 1) -- TestBit requires 1-based index
|
|
end
|
|
|
|
-- Request a desired route for an input chord (key + mods).
|
|
-- Return true if the route is available this frame.
|
|
-- - Routes and key ownership are attributed at the beginning of next frame based on best score and mod state.
|
|
-- (Conceptually this does a "Submit for next frame" + "Test for current frame".
|
|
-- As such, it could be called TrySetXXX or SubmitXXX, or the Submit and Test operations should be separate.)
|
|
--- @param key_chord ImGuiKeyChord
|
|
--- @param flags ImGuiInputFlags
|
|
--- @param owner_id ImGuiID
|
|
function ImGui.SetShortcutRouting(key_chord, flags, owner_id)
|
|
local g = GImGui
|
|
|
|
if bit32.band(flags, ImGuiInputFlags.RouteTypeMask_) == 0 then
|
|
flags = bit32.bor(flags, ImGuiInputFlags.RouteGlobal, ImGuiInputFlags.RouteOverFocused, ImGuiInputFlags.RouteOverActive) -- IMPORTANT: This is the default for SetShortcutRouting() but NOT Shortcut()
|
|
else
|
|
IM_ASSERT(ImIsPowerOfTwo(bit32.band(flags, ImGuiInputFlags.RouteTypeMask_))) -- Check that only 1 routing flag is used
|
|
end
|
|
IM_ASSERT(owner_id ~= ImGuiKeyOwner_Any and owner_id ~= ImGuiKeyOwner_NoOwner)
|
|
if bit32.band(flags, bit32.bor(ImGuiInputFlags.RouteOverFocused, ImGuiInputFlags.RouteUnlessBgFocused)) ~= 0 then
|
|
IM_ASSERT(bit32.band(flags, ImGuiInputFlags.RouteGlobal) ~= 0)
|
|
end
|
|
if bit32.band(flags, ImGuiInputFlags.RouteOverActive) ~= 0 then
|
|
IM_ASSERT(bit32.band(flags, bit32.bor(ImGuiInputFlags.RouteGlobal, ImGuiInputFlags.RouteFocused)) ~= 0)
|
|
end
|
|
|
|
-- Add ImGuiMod_XXXX when a corresponding ImGuiKey.LeftXXX/ImGuiKey.RightXXX is specified.
|
|
key_chord = ImGui.FixupKeyChord(key_chord)
|
|
|
|
-- [DEBUG] Debug break requested by user
|
|
if g.DebugBreakInShortcutRouting == key_chord then
|
|
-- IM_DEBUG_BREAK()
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiInputFlags.RouteUnlessBgFocused) ~= 0 then
|
|
if g.NavWindow == nil then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Note how ImGuiInputFlags.RouteAlways won't set routing and thus won't set owner. May want to rework this?
|
|
if bit32.band(flags, ImGuiInputFlags.RouteAlways) ~= 0 then
|
|
-- IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> always, no register", ImGui.GetKeyChordName(key_chord), flags, owner_id)
|
|
return true
|
|
end
|
|
|
|
-- Specific culling when there's an active item.
|
|
if g.ActiveId ~= 0 and g.ActiveId ~= owner_id then
|
|
if bit32.band(flags, ImGuiInputFlags.RouteActive) ~= 0 then
|
|
return false
|
|
end
|
|
|
|
-- Cull shortcuts with no modifiers when it could generate a character.
|
|
-- e.g. Shortcut(ImGuiKey.G) also generates 'g' character, should not trigger when InputText() is active.
|
|
-- but Shortcut(Ctrl+G) should generally trigger when InputText() is active.
|
|
-- TL;DR: lettered shortcut with no mods or with only Alt mod will not trigger while an item reading text input is active.
|
|
-- (We cannot filter based on io.InputQueueCharacters[] contents because of trickling and key<>chars submission order are undefined)
|
|
if g.IO.WantTextInput and IsKeyChordPotentiallyCharInput(key_chord) then
|
|
-- IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> filtered as potential char input", ImGui.GetKeyChordName(key_chord), flags, owner_id)
|
|
return false
|
|
end
|
|
|
|
-- ActiveIdUsingAllKeyboardKeys trumps all for ActiveId
|
|
if bit32.band(flags, ImGuiInputFlags.RouteOverActive) == 0 and g.ActiveIdUsingAllKeyboardKeys then
|
|
local key = bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_))
|
|
if key == ImGuiKey.None then
|
|
key = ImGui.ConvertSingleModFlagToKey(bit32.band(key_chord, ImGuiMod_Mask_))
|
|
end
|
|
if key >= ImGuiKey_Keyboard_BEGIN and key < ImGuiKey_Keyboard_END then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Where do we evaluate route for?
|
|
local focus_scope_id = g.CurrentFocusScopeId
|
|
if bit32.band(flags, ImGuiInputFlags.RouteFromRootWindow) ~= 0 then
|
|
focus_scope_id = g.CurrentWindow.RootWindow.ID -- See PushFocusScope() call in Begin()
|
|
end
|
|
|
|
local score = CalcRoutingScore(focus_scope_id, owner_id, flags)
|
|
-- IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> score %d", ImGui.GetKeyChordName(key_chord), flags, owner_id, score)
|
|
if score == 0 then
|
|
return false
|
|
end
|
|
|
|
-- Submit routing for NEXT frame (assuming score is sufficient)
|
|
-- FIXME: Could expose a way to use a "serve last" policy for same score resolution (using >= instead of >).
|
|
local routing_data = ImGui.GetShortcutRoutingData(key_chord)
|
|
-- const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score >= routing_data->RoutingNextScore) : (score > routing_data->RoutingNextScore);
|
|
if score > routing_data.RoutingNextScore then
|
|
routing_data.RoutingNext = owner_id
|
|
routing_data.RoutingNextScore = score
|
|
end
|
|
|
|
-- Return routing state for CURRENT frame
|
|
if routing_data.RoutingCurr == owner_id then
|
|
-- IMGUI_DEBUG_LOG_INPUTROUTING("--> granting current route")
|
|
end
|
|
return routing_data.RoutingCurr == owner_id
|
|
end
|
|
|
|
-- We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position.
|
|
--- @param mouse_pos? ImVec2
|
|
function ImGui.IsMousePosValid(mouse_pos)
|
|
local MOUSE_INVALID = -256000.0
|
|
local p
|
|
if mouse_pos then p = mouse_pos else p = GImGui.IO.MousePos end
|
|
return p.x >= MOUSE_INVALID and p.y >= MOUSE_INVALID
|
|
end
|
|
|
|
-- [WILL OBSOLETE] This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid.
|
|
--- @return bool
|
|
function ImGui.IsAnyMouseDown()
|
|
local g = GImGui
|
|
for n = 0, 2 do -- IM_COUNTOF(g.IO.MouseDown) - 1
|
|
if g.IO.MouseDown[n] then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
--- @param owner_id? ImGuiID
|
|
function ImGui.IsMouseDown(button, owner_id)
|
|
if owner_id == nil then owner_id = ImGuiKeyOwner_Any end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3) -- IM_COUNTOF(g.IO.MouseDown)
|
|
return g.IO.MouseDown[button] and ImGui.TestKeyOwner(ImGui.MouseButtonToKey(button), owner_id) -- Should be same as IsKeyDown(MouseButtonToKey(button), owner_id), but this allows legacy code hijacking the io.Mousedown[] array.
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
--- @param is_repeat? bool
|
|
function ImGui.IsMouseClicked(button, is_repeat)
|
|
if is_repeat == nil then is_repeat = false end
|
|
|
|
return ImGui.IsMouseClickedEx(button, is_repeat and ImGuiInputFlags.Repeat or ImGuiInputFlags.None, ImGuiKeyOwner_Any)
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
--- @param flags ImGuiInputFlags
|
|
--- @param owner_id? ImGuiID
|
|
@native
|
|
function ImGui.IsMouseClickedEx(button, flags, owner_id)
|
|
if owner_id == nil then owner_id = ImGuiKeyOwner_Any end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3) -- IM_COUNTOF(g.IO.MouseDown)
|
|
|
|
if not g.IO.MouseDown[button] then -- In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership)
|
|
return false
|
|
end
|
|
local t = g.IO.MouseDownDuration[button]
|
|
if t < 0.0 then
|
|
return false
|
|
end
|
|
IM_ASSERT(bit32.band(flags, bit32.bnot(ImGuiInputFlags.SupportedByIsMouseClicked)) == 0) -- Passing flags not supported by this function! -- FIXME: Could support RepeatRate and RepeatUntil flags here.
|
|
|
|
local is_repeat = (bit32.band(flags, ImGuiInputFlags.Repeat) ~= 0)
|
|
local pressed = (t == 0.0) or (is_repeat and t > g.IO.KeyRepeatDelay and ImGui.CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0)
|
|
|
|
if not pressed then
|
|
return false
|
|
end
|
|
|
|
if not ImGui.TestKeyOwner(ImGui.MouseButtonToKey(button), owner_id) then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
--- @param owner_id? ImGuiID
|
|
function ImGui.IsMouseReleased(button, owner_id)
|
|
if owner_id == nil then owner_id = ImGuiKeyOwner_Any end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3) -- IM_COUNTOF(g.IO.MouseDown)
|
|
return g.IO.MouseReleased[button] and ImGui.TestKeyOwner(ImGui.MouseButtonToKey(button), owner_id) -- Should be same as IsKeyReleased(MouseButtonToKey(button), owner_id)
|
|
end
|
|
|
|
-- Prefer higher-level helper GetItemClickedCountWithSingleClickDelay()
|
|
-- Use if you absolutely need to distinguish single-click from double-click by introducing a delay.
|
|
-- Generally use with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount == 1' test.
|
|
-- This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename.
|
|
--- @param button ImGuiMouseButton
|
|
--- @param delay? float
|
|
function ImGui.IsMouseReleasedWithDelay(button, delay)
|
|
if delay == nil then delay = -1.0 end
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3)
|
|
if ImGui.IsMouseDown(button) then
|
|
return false
|
|
end
|
|
if delay < 0.0 then
|
|
delay = g.IO.MouseSingleClickDelay or 0.50
|
|
end
|
|
local time_since_release = g.Time - g.IO.MouseReleasedTime[button]
|
|
return (time_since_release - g.IO.DeltaTime < delay) and (time_since_release >= delay)
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
--- @param owner_id? ImGuiID
|
|
function ImGui.IsMouseDoubleClicked(button, owner_id)
|
|
if owner_id == nil then owner_id = ImGuiKeyOwner_Any end
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3)
|
|
return g.IO.MouseClickedCount[button] == 2 and ImGui.TestKeyOwner(ImGui.MouseButtonToKey(button), owner_id)
|
|
end
|
|
|
|
--- @param button ImGuiMouseButton
|
|
function ImGui.GetMouseClickedCount(button)
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3)
|
|
return g.IO.MouseClickedCount[button]
|
|
end
|
|
|
|
-- FIXME: This is close to what BeginDragDropSource() is doing, maybe rework.
|
|
--- @param mouse_button ImGuiMouseButton
|
|
local function LastItemOverlayButtonForNullId(mouse_button)
|
|
local g = GImGui
|
|
IM_ASSERT(g.LastItemData.ID == 0)
|
|
local window = g.CurrentWindow
|
|
local id = window:GetIDFromRectangle(g.LastItemData.Rect)
|
|
if g.IO.MouseClicked[mouse_button] and ImGui.ItemHoverable(g.LastItemData.Rect, id, g.LastItemData.ItemFlags) then
|
|
ImGui.SetActiveID(id, window)
|
|
ImGui.FocusWindow(window)
|
|
elseif g.ActiveId == id then
|
|
ImGui.KeepAliveID(id)
|
|
if not g.IO.MouseDown[mouse_button] then
|
|
ImGui.ClearActiveID()
|
|
end
|
|
end
|
|
return id
|
|
end
|
|
|
|
-- [BETA] Building block for disambiguation between single-click and double-click.
|
|
-- - Returns 1 on single-click but delayed by io.MouseSingleClickDelay (which is always > io.MouseDoubleClickTime) after mouse release.
|
|
-- - Returns 2+ on double-click and subsequent repeated clicks.
|
|
-- In order use that to replicate Windows Explorer's "click on label to rename after a delay",
|
|
-- When the function returns 1 for a delayed single click, you can add with further tests:
|
|
-- - If you want to test that the mouse position AT THE TIME of the click (before the delay): you can use the 'io.MouseClickedPos[mouse_button]' position.
|
|
-- - If you want to test that the mouse position is still over the item at the end of delay: you can use '&& IsItemHovered()'.
|
|
-- - If you want to test that the item was selected or the sole selection AT THE TIME of the click (before the delay): you can test for 'g.LastActiveIdWasSelected' or 'g.LastActiveIdWasSoleSelected'.
|
|
-- e.g.
|
|
-- local click_count = ImGui.GetItemClickedCountWithSingleClickDelay(mouse_button)
|
|
-- if click_count == 1 and ImGui.GetCurrentContext().LastActiveIdWasSoleSelected then
|
|
-- StartRename()
|
|
-- end
|
|
-- if click_count == 2 then
|
|
-- Launch()
|
|
-- end
|
|
--- @param mouse_button? ImGuiMouseButton
|
|
--- @param delay? float
|
|
--- @return int
|
|
function ImGui.GetItemClickedCountWithSingleClickDelay(mouse_button, delay)
|
|
if mouse_button == nil then mouse_button = 0 end
|
|
if delay == nil then delay = -1.0 end
|
|
-- Action: double-click and subsequent clicks
|
|
local g = GImGui
|
|
if g.IO.MouseClickedCount[mouse_button] >= 2 and ImGui.IsItemClicked(mouse_button) then
|
|
return g.IO.MouseClickedCount[mouse_button]
|
|
end
|
|
|
|
-- Action: second click, delayed
|
|
local id = g.LastItemData.ID
|
|
if id == 0 then
|
|
id = LastItemOverlayButtonForNullId(mouse_button)
|
|
end
|
|
if g.LastActiveId == id then
|
|
if delay >= 0.0 then
|
|
delay = ImMax(delay, g.IO.MouseDoubleClickTime + 0.01)
|
|
end
|
|
if ImGui.IsMouseReleasedWithDelay(mouse_button, delay) and g.IO.MouseClickedLastCount[mouse_button] == 1 then
|
|
return 1
|
|
end
|
|
end
|
|
return 0
|
|
end
|
|
|
|
--- @return ImVec2 # Ref
|
|
function ImGui.GetMousePos()
|
|
return GImGui.IO.MousePos
|
|
end
|
|
|
|
--- @return ImVec2
|
|
function ImGui.GetMousePosOnOpeningCurrentPopup()
|
|
local g = GImGui
|
|
if g.BeginPopupStack.Size > 0 then
|
|
return g.BeginPopupStack.Data[g.BeginPopupStack.Size].OpenMousePos
|
|
end
|
|
return g.IO.MousePos
|
|
end
|
|
|
|
-- Return the delta from the initial clicking position while the mouse button is clicked or was just released.
|
|
-- This is locked and return 0.0f until the mouse moves past a distance threshold at least once.
|
|
-- NB: This is only valid if IsMousePosValid(). backends in theory should always keep mouse position valid when dragging even outside the client window.
|
|
--- @param button? ImGuiMouseButton
|
|
--- @param lock_threshold? float
|
|
--- @return ImVec2
|
|
function ImGui.GetMouseDragDelta(button, lock_threshold)
|
|
if button == nil then button = 0 end
|
|
if lock_threshold == nil then lock_threshold = -1.0 end
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3)
|
|
if lock_threshold < 0.0 then
|
|
lock_threshold = g.IO.MouseDragThreshold
|
|
end
|
|
if g.IO.MouseDown[button] or g.IO.MouseReleased[button] then
|
|
if g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold then
|
|
if ImGui.IsMousePosValid(g.IO.MousePos) and ImGui.IsMousePosValid(g.IO.MouseClickedPos[button]) then
|
|
return g.IO.MousePos - g.IO.MouseClickedPos[button]
|
|
end
|
|
end
|
|
end
|
|
return ImVec2(0.0, 0.0)
|
|
end
|
|
|
|
--- @param button? ImGuiMouseButton
|
|
function ImGui.ResetMouseDragDelta(button)
|
|
if button == nil then button = 0 end
|
|
local g = GImGui
|
|
IM_ASSERT(button >= 0 and button < 3)
|
|
-- NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr
|
|
ImVec2_Copy(g.IO.MouseClickedPos[button], g.IO.MousePos)
|
|
end
|
|
|
|
do
|
|
|
|
local key_changed_mask = ImBitArray(ImGuiKey.NamedKey_COUNT)
|
|
|
|
-- Process input queue
|
|
-- We always call this with the value of 'bool g.IO.ConfigInputTrickleEventQueue'.
|
|
-- - trickle_fast_inputs = false : process all events, turn into flattened input state (e.g. successive down/up/down/up will be lost)
|
|
-- - trickle_fast_inputs = true : process as many events as possible (successive down/up/down/up will be trickled over several frames so nothing is lost) (new feature in 1.87)
|
|
--- @param trickle_fast_inputs bool
|
|
function ImGui.UpdateInputEvents(trickle_fast_inputs)
|
|
local g = GImGui
|
|
local io = g.IO
|
|
|
|
-- Only trickle chars<>key when working with InputText()
|
|
-- FIXME: InputText() could parse event trail?
|
|
-- FIXME: Could specialize chars<>keys trickling rules for control keys (those not typically associated to characters)
|
|
local trickle_interleaved_nonchar_keys_and_text = trickle_fast_inputs and g.WantTextInputNextFrame == 1
|
|
|
|
local mouse_moved = false
|
|
local mouse_wheeled = false
|
|
local key_changed = false
|
|
local key_changed_nonchar = false
|
|
local text_inputted = false
|
|
local mouse_button_changed = 0x00
|
|
|
|
key_changed_mask:ClearAllBits()
|
|
|
|
local event_n = 1
|
|
local stop_3231 = false
|
|
while event_n <= g.InputEventsQueue.Size do
|
|
repeat
|
|
local e = g.InputEventsQueue.Data[event_n]
|
|
if e.Type == ImGuiInputEventType.MousePos then
|
|
if g.IO.WantSetMousePos then
|
|
break
|
|
end
|
|
-- Trickling Rule: Stop processing queued events if we already handled a mouse button change
|
|
if trickle_fast_inputs and (mouse_button_changed ~= 0 or mouse_wheeled or key_changed or text_inputted) then
|
|
stop_3231 = true; break
|
|
end
|
|
local event_pos = ImVec2(e.MousePos.PosX, e.MousePos.PosY)
|
|
io.MousePos = event_pos
|
|
io.MouseSource = e.MousePos.MouseSource
|
|
mouse_moved = true
|
|
elseif e.Type == ImGuiInputEventType.MouseButton then
|
|
-- Trickling Rule: Stop processing queued events if we got multiple action on the same button
|
|
local button = e.MouseButton.Button
|
|
IM_ASSERT(button >= 0 and button < ImGuiMouseButton.COUNT)
|
|
if trickle_fast_inputs and ((bit32.band(mouse_button_changed, bit32.lshift(1, button)) ~= 0) or mouse_wheeled) then
|
|
stop_3231 = true; break
|
|
end
|
|
if trickle_fast_inputs and e.MouseButton.MouseSource == ImGuiMouseSource.TouchScreen and mouse_moved then -- #2702: TouchScreen have no initial hover.
|
|
stop_3231 = true; break
|
|
end
|
|
|
|
io.MouseDown[button] = e.MouseButton.Down
|
|
io.MouseSource = e.MouseButton.MouseSource
|
|
mouse_button_changed = bit32.bor(mouse_button_changed, bit32.lshift(1, button))
|
|
elseif e.Type == ImGuiInputEventType.MouseWheel then
|
|
-- Trickling Rule: Stop processing queued events if we got multiple action on the event
|
|
if trickle_fast_inputs and (mouse_moved or mouse_button_changed ~= 0) then
|
|
stop_3231 = true; break
|
|
end
|
|
|
|
io.MouseWheelH = io.MouseWheelH + e.MouseWheel.WheelX
|
|
io.MouseWheel = io.MouseWheel + e.MouseWheel.WheelY
|
|
io.MouseSource = e.MouseWheel.MouseSource
|
|
mouse_wheeled = true
|
|
elseif e.Type == ImGuiInputEventType.MouseViewport then
|
|
io.MouseHoveredViewport = e.MouseViewport.HoveredViewportID
|
|
elseif e.Type == ImGuiInputEventType.Key then
|
|
-- Trickling Rule: Stop processing queued events if we got multiple action on the same button
|
|
if bit32.band(io.ConfigFlags, ImGuiConfigFlags.NoKeyboard) ~= 0 then
|
|
break
|
|
end
|
|
|
|
local key = e.Key.Key
|
|
IM_ASSERT(key ~= ImGuiKey.None)
|
|
|
|
-- TODO: Also gets 0-based index here, consider making key_data storage 1-based
|
|
local key_data, key_data_index = ImGui.GetKeyData(g, key)
|
|
|
|
if trickle_fast_inputs and key_data.Down ~= e.Key.Down and (key_changed_mask:TestBit(key_data_index + 1) or mouse_button_changed ~= 0) then
|
|
stop_3231 = true; break
|
|
end
|
|
|
|
local key_is_potentially_for_char_input = IsKeyChordPotentiallyCharInput(bit32.bor(ImGui.GetMergedModsFromKeys(), key))
|
|
if trickle_interleaved_nonchar_keys_and_text and (text_inputted and not key_is_potentially_for_char_input) then
|
|
stop_3231 = true; break
|
|
end
|
|
|
|
if key_data.Down ~= e.Key.Down then -- Analog change only do not trigger this, so it won't block e.g. further mouse pos events testing key_changed.
|
|
key_changed = true
|
|
key_changed_mask:SetBit(key_data_index + 1)
|
|
if trickle_interleaved_nonchar_keys_and_text and not key_is_potentially_for_char_input then
|
|
key_changed_nonchar = true
|
|
end
|
|
end
|
|
|
|
key_data.Down = e.Key.Down
|
|
key_data.AnalogValue = e.Key.AnalogValue
|
|
elseif e.Type == ImGuiInputEventType.Text then
|
|
if bit32.band(io.ConfigFlags, ImGuiConfigFlags.NoKeyboard) ~= 0 then
|
|
break
|
|
end
|
|
-- Trickling Rule: Stop processing queued events if keys/mouse have been interacted with
|
|
if trickle_fast_inputs and (mouse_button_changed ~= 0 or mouse_moved or mouse_wheeled) then
|
|
stop_3231 = true; break
|
|
end
|
|
if trickle_interleaved_nonchar_keys_and_text and key_changed_nonchar then
|
|
stop_3231 = true; break
|
|
end
|
|
local c = e.Text.Char
|
|
io.InputQueueCharacters:push_back((c <= IM_UNICODE_CODEPOINT_MAX) and c or IM_UNICODE_CODEPOINT_INVALID)
|
|
if trickle_interleaved_nonchar_keys_and_text then
|
|
text_inputted = true
|
|
end
|
|
elseif e.Type == ImGuiInputEventType.Focus then
|
|
-- We intentionally overwrite this and process in NewFrame(), in order to give a chance
|
|
-- to multi-viewports backends to queue AddFocusEvent(false) + AddFocusEvent(true) in same frame.
|
|
local focus_lost = not e.AppFocused.Focused
|
|
io.AppFocusLost = focus_lost
|
|
else
|
|
IM_ASSERT(false, "Unknown event!")
|
|
end
|
|
|
|
until true
|
|
if stop_3231 then break end
|
|
|
|
event_n = event_n + 1
|
|
end
|
|
|
|
-- Remaining events will be processed on the next frame
|
|
-- FIXME-MULTITHREADING: io.AddKeyEvent() etc. calls are mostly thread-safe apart from the fact they push to this
|
|
-- queue which may be resized here. Could potentially rework this to narrow down the section needing a mutex? (#5772)
|
|
if event_n == g.InputEventsQueue.Size + 1 then
|
|
g.InputEventsQueue:resize(0)
|
|
else
|
|
for i = 1, event_n - 1 do
|
|
g.InputEventsQueue:erase(1)
|
|
end
|
|
end
|
|
|
|
-- Clear buttons state when focus is lost
|
|
-- - this is useful so e.g. releasing Alt after focus loss on Alt-Tab doesn't trigger the Alt menu toggle.
|
|
-- - we clear in EndFrame() and not now in order allow application/user code polling this flag
|
|
-- (e.g. custom backend may want to clear additional data, custom widgets may want to react with a "canceling" event).
|
|
if g.IO.AppFocusLost then
|
|
g.IO:ClearInputKeys()
|
|
g.IO:ClearInputMouse()
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- _LockXXX flags are useful to lock keys away from code which is not input-owner aware.
|
|
-- When using _LockXXX flags, you can use ImGuiKeyOwner_Any to lock keys from everyone.
|
|
-- - SetKeyOwner(..., None) : clears owner
|
|
-- - SetKeyOwner(..., Any, !Lock) : illegal (assert)
|
|
-- - SetKeyOwner(..., Any or None, Lock) : set lock
|
|
-- Ownership is automatically released on the frame after a release, see code in UpdateKeyboardInputs().
|
|
--- @param key ImGuiKey
|
|
--- @param owner_id ImGuiID
|
|
--- @param flags? ImGuiInputFlags
|
|
function ImGui.SetKeyOwner(key, owner_id, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui --- @cast g ImGuiContext
|
|
IM_ASSERT(ImGui.IsNamedKeyOrMod(key) and (owner_id ~= ImGuiKeyOwner_Any or (bit32.band(flags, bit32.bor(ImGuiInputFlags.LockThisFrame, ImGuiInputFlags.LockUntilRelease)) ~= 0)), "Can only use _Any with _LockXXX flags (to eat a key away without an ID to retrieve it)")
|
|
IM_ASSERT(bit32.band(flags, bit32.bnot(ImGuiInputFlags.SupportedBySetKeyOwner)) == 0, "Passing flags not supported by this function!")
|
|
-- IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X, flags=%08X)\n", GetKeyName(key), owner_id, flags)
|
|
|
|
local owner_data = ImGui.GetKeyOwnerData(g, key)
|
|
owner_data.OwnerCurr = owner_id
|
|
owner_data.OwnerNext = owner_id
|
|
|
|
-- We cannot lock by default as it would likely break lots of legacy code.
|
|
-- In the case of using LockUntilRelease while key is not down we still lock during the frame (no key_data.Down test)
|
|
owner_data.LockUntilRelease = (bit32.band(flags, ImGuiInputFlags.LockUntilRelease) ~= 0)
|
|
owner_data.LockThisFrame = (bit32.band(flags, ImGuiInputFlags.LockThisFrame) ~= 0) or owner_data.LockUntilRelease
|
|
end
|
|
|
|
-- Rarely used helper
|
|
--- @param key_chord ImGuiKeyChord
|
|
--- @param owner_id ImGuiID
|
|
--- @param flags? ImGuiInputFlags
|
|
function ImGui.SetKeyOwnersForKeyChord(key_chord, owner_id, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
if bit32.band(key_chord, ImGuiMod_Ctrl) ~= 0 then
|
|
ImGui.SetKeyOwner(ImGuiMod_Ctrl, owner_id, flags)
|
|
end
|
|
if bit32.band(key_chord, ImGuiMod_Shift) ~= 0 then
|
|
ImGui.SetKeyOwner(ImGuiMod_Shift, owner_id, flags)
|
|
end
|
|
if bit32.band(key_chord, ImGuiMod_Alt) ~= 0 then
|
|
ImGui.SetKeyOwner(ImGuiMod_Alt, owner_id, flags)
|
|
end
|
|
if bit32.band(key_chord, ImGuiMod_Super) ~= 0 then
|
|
ImGui.SetKeyOwner(ImGuiMod_Super, owner_id, flags)
|
|
end
|
|
if bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_)) ~= 0 then
|
|
ImGui.SetKeyOwner(bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_)), owner_id, flags)
|
|
end
|
|
end
|
|
|
|
-- This is more or less equivalent to a fancier version of:
|
|
-- if (IsItemHovered() || IsItemActive())
|
|
-- SetKeyOwner(key, GetItemID());
|
|
-- Extensive uses of that (e.g. many calls for a single item) may want to manually perform the tests once and then call SetKeyOwner() multiple times.
|
|
-- More advanced usage scenarios may want to call SetKeyOwner() manually based on different condition.
|
|
-- Worth noting is that only one item can be hovered and only one item can be active, therefore this usage pattern doesn't need to bother with routing and priority.
|
|
--- @param key ImGuiKey
|
|
--- @param flags? ImGuiInputFlags
|
|
function ImGui.SetItemKeyOwner(key, flags)
|
|
if flags == nil then flags = ImGuiInputFlags.None end
|
|
|
|
local g = GImGui
|
|
local id = g.LastItemData.ID
|
|
if id == 0 or (g.HoveredId ~= id and g.ActiveId ~= id) then
|
|
return false
|
|
end
|
|
if bit32.band(flags, ImGuiInputFlags.CondMask_) == 0 then
|
|
flags = bit32.bor(flags, ImGuiInputFlags.CondDefault_)
|
|
end
|
|
if (g.HoveredId == id and bit32.band(flags, ImGuiInputFlags.CondHovered) ~= 0) or (g.ActiveId == id and bit32.band(flags, ImGuiInputFlags.CondActive) ~= 0) then
|
|
IM_ASSERT(bit32.band(flags, bit32.bnot(ImGuiInputFlags.SupportedBySetItemKeyOwner)) == 0) -- Passing flags not supported by this function!
|
|
if not ImGui.TestKeyOwner(key, id) then
|
|
return false
|
|
end
|
|
ImGui.SetKeyOwner(key, id, bit32.band(flags, bit32.bnot(ImGuiInputFlags.CondMask_)))
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- This is equivalent to comparing KeyMods + doing a IsKeyPressed()
|
|
--- @param key_chord ImGuiKeyChord
|
|
--- @param flags ImGuiInputFlags
|
|
--- @param owner_id ImGuiID
|
|
function ImGui.IsKeyChordPressed(key_chord, flags, owner_id)
|
|
local g = GImGui
|
|
key_chord = ImGui.FixupKeyChord(key_chord)
|
|
local mods = bit32.band(key_chord, ImGuiMod_Mask_)
|
|
if g.IO.KeyMods ~= mods then
|
|
return false
|
|
end
|
|
|
|
-- Special storage location for mods
|
|
local key = bit32.band(key_chord, bit32.bnot(ImGuiMod_Mask_))
|
|
if key == ImGuiKey.None then
|
|
key = ImGui.ConvertSingleModFlagToKey(mods)
|
|
end
|
|
if not ImGui.IsKeyPressedEx(key, bit32.band(flags, ImGuiInputFlags.RepeatMask_), owner_id) then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
--- @param key_chord ImGuiKeyChord
|
|
--- @param flags? ImGuiInputFlags
|
|
function ImGui.SetNextItemShortcut(key_chord, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui
|
|
g.NextItemData.HasFlags = bit32.bor(g.NextItemData.HasFlags, ImGuiNextItemDataFlags.HasShortcut)
|
|
g.NextItemData.Shortcut = key_chord
|
|
g.NextItemData.ShortcutFlags = flags
|
|
end
|
|
|
|
--- @param key_chord ImGuiKeyChord
|
|
--- @param flags ImGuiInputFlags
|
|
--- @param owner_id ImGuiID
|
|
function ImGui.Shortcut(key_chord, flags, owner_id)
|
|
local g = GImGui
|
|
-- IMGUI_DEBUG_LOG("Shortcut(%s, flags=%X, owner_id=0x%08X)\n", ImGui.GetKeyChordName(key_chord, g.TempBuffer.Data, g.TempBuffer.Size), flags, owner_id)
|
|
|
|
-- When using (owner_id == 0/Any): SetShortcutRouting() will use CurrentFocusScopeId and filter with this, so IsKeyPressed() is fine with he 0/Any.
|
|
if bit32.band(flags, ImGuiInputFlags.RouteTypeMask_) == 0 then
|
|
flags = bit32.bor(flags, ImGuiInputFlags.RouteFocused)
|
|
end
|
|
|
|
-- Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default)
|
|
-- Effectively makes Shortcut() always input-owner aware.
|
|
if owner_id == ImGuiKeyOwner_Any or owner_id == ImGuiKeyOwner_NoOwner then
|
|
owner_id = GetRoutingIdFromOwnerId(owner_id)
|
|
end
|
|
|
|
if bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) ~= 0 then
|
|
return false
|
|
end
|
|
|
|
-- Submit route
|
|
if not ImGui.SetShortcutRouting(key_chord, flags, owner_id) then
|
|
return false
|
|
end
|
|
|
|
-- Default repeat behavior for Shortcut()
|
|
-- So e.g. pressing Ctrl+W and releasing Ctrl while holding W will not trigger the W shortcut.
|
|
if bit32.band(flags, ImGuiInputFlags.Repeat) ~= 0 and bit32.band(flags, ImGuiInputFlags.RepeatUntilMask_) == 0 then
|
|
flags = bit32.bor(flags, ImGuiInputFlags.RepeatUntilKeyModsChange)
|
|
end
|
|
|
|
if not ImGui.IsKeyChordPressed(key_chord, flags, owner_id) then
|
|
return false
|
|
end
|
|
|
|
-- Claim mods during the press
|
|
ImGui.SetKeyOwnersForKeyChord(bit32.band(key_chord, ImGuiMod_Mask_), owner_id)
|
|
|
|
IM_ASSERT(bit32.band(flags, bit32.bnot(ImGuiInputFlags.SupportedByShortcut)) == 0) -- Passing flags not supported by this function!
|
|
return true
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return bool
|
|
function ImGui.IsWindowActiveAndVisible(window)
|
|
return window.Active and not window.Hidden
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @nodiscard
|
|
local function CalcWindowMinSize(window)
|
|
-- We give windows non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups)
|
|
-- FIXME: Essentially we want to restrict manual resizing to WindowMinSize+Decoration, and allow api resizing to be smaller.
|
|
-- Perhaps should tend further a neater test for this.
|
|
local g = GImGui
|
|
|
|
local size_min = ImVec2()
|
|
|
|
if (bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0) and (bit32.band(window.Flags, ImGuiWindowFlags.Popup) == 0) then
|
|
if bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeX) ~= 0 then
|
|
size_min.x = g.Style.WindowMinSize.x
|
|
else
|
|
size_min.x = IMGUI_WINDOW_HARD_MIN_SIZE
|
|
end
|
|
|
|
if bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeY) ~= 0 then
|
|
size_min.y = g.Style.WindowMinSize.y
|
|
else
|
|
size_min.y = IMGUI_WINDOW_HARD_MIN_SIZE
|
|
end
|
|
else
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.AlwaysAutoResize) == 0 then
|
|
size_min.x = g.Style.WindowMinSize.x
|
|
else
|
|
size_min.x = IMGUI_WINDOW_HARD_MIN_SIZE
|
|
end
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.AlwaysAutoResize) == 0 then
|
|
size_min.y = g.Style.WindowMinSize.y
|
|
else
|
|
size_min.y = IMGUI_WINDOW_HARD_MIN_SIZE
|
|
end
|
|
end
|
|
|
|
-- Reduce artifacts with very small windows
|
|
local window_for_height = window
|
|
size_min.y = ImMax(size_min.y, window_for_height.TitleBarHeight + window_for_height.MenuBarHeight + ImMax(0, g.Style.WindowRounding - 1))
|
|
|
|
return size_min
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param size_desired ImVec2
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
local function CalcWindowSizeAfterConstraint(window, size_desired)
|
|
local g = GImGui
|
|
|
|
local new_size = ImVec2()
|
|
ImVec2_Copy(new_size, size_desired)
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSizeConstraint) ~= 0 then
|
|
-- See comments in SetNextWindowSizeConstraints() for details about setting size_min an size_max.
|
|
local cr = ImRect()
|
|
ImRect_Copy(cr, g.NextWindowData.SizeConstraintRect)
|
|
|
|
new_size.x = (cr.Min.x >= 0 and cr.Max.x >= 0) and ImClamp(new_size.x, cr.Min.x, cr.Max.x) or window.SizeFull.x
|
|
new_size.y = (cr.Min.y >= 0 and cr.Max.y >= 0) and ImClamp(new_size.y, cr.Min.y, cr.Max.y) or window.SizeFull.y
|
|
if g.NextWindowData.SizeCallback then
|
|
local data = {} -- TODO: ImGuiSizeCallbackData
|
|
data.UserData = g.NextWindowData.SizeCallbackUserData
|
|
data.Pos = ImVec2(window.Pos.x, window.Pos.y)
|
|
data.CurrentSize = ImVec2(window.SizeFull.x, window.SizeFull.y)
|
|
data.DesiredSize = ImVec2(new_size.x, new_size.y)
|
|
g.NextWindowData.SizeCallback(data)
|
|
new_size.x = data.DesiredSize.x
|
|
new_size.y = data.DesiredSize.y
|
|
end
|
|
new_size.x = IM_TRUNC(new_size.x)
|
|
new_size.y = IM_TRUNC(new_size.y)
|
|
end
|
|
|
|
-- Minimum size
|
|
local size_min = CalcWindowMinSize(window)
|
|
return ImMax(new_size, size_min)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param content_size_current ImVec2
|
|
--- @param content_size_ideal ImVec2
|
|
local function CalcWindowContentSizes(window, content_size_current, content_size_ideal)
|
|
local preserve_old_content_sizes = false
|
|
if (window.Collapsed and window.AutoFitFramesX <= 0 and window.AutoFitFramesY <= 0) then
|
|
preserve_old_content_sizes = true
|
|
elseif (window.Hidden and window.HiddenFramesCannotSkipItems == 0 and window.HiddenFramesCanSkipItems > 0) then
|
|
preserve_old_content_sizes = true
|
|
end
|
|
if preserve_old_content_sizes then
|
|
content_size_current.x = window.ContentSize.x
|
|
content_size_current.y = window.ContentSize.y
|
|
content_size_ideal.x = window.ContentSizeIdeal.x
|
|
content_size_ideal.y = window.ContentSizeIdeal.y
|
|
return
|
|
end
|
|
|
|
if (window.ContentSizeExplicit.x ~= 0.0) then content_size_current.x = window.ContentSizeExplicit.x else content_size_current.x = ImTrunc64(window.DC.CursorMaxPos.x - window.DC.CursorStartPos.x) end
|
|
if (window.ContentSizeExplicit.y ~= 0.0) then content_size_current.y = window.ContentSizeExplicit.y else content_size_current.y = ImTrunc64(window.DC.CursorMaxPos.y - window.DC.CursorStartPos.y) end
|
|
if (window.ContentSizeExplicit.x ~= 0.0) then content_size_ideal.x = window.ContentSizeExplicit.x else content_size_ideal.x = ImTrunc64(ImMax(window.DC.CursorMaxPos.x, window.DC.IdealMaxPos.x) - window.DC.CursorStartPos.x) end
|
|
if (window.ContentSizeExplicit.y ~= 0.0) then content_size_ideal.y = window.ContentSizeExplicit.y else content_size_ideal.y = ImTrunc64(ImMax(window.DC.CursorMaxPos.y, window.DC.IdealMaxPos.y) - window.DC.CursorStartPos.y) end
|
|
end
|
|
|
|
--- @param window ImGuiWindow # The window to calculate auto-fit size for
|
|
--- @param size_contents ImVec2 # The content size
|
|
--- @param axis_mask int # The axis mask to determine which axes to auto-fit
|
|
--- @return ImVec2 # The auto-fit size
|
|
local function CalcWindowAutoFitSize(window, size_contents, axis_mask)
|
|
local g = GImGui
|
|
local style = g.Style
|
|
local decoration_w_without_scrollbars = window.DecoOuterSizeX1 + window.DecoOuterSizeX2 - window.ScrollbarSizes.x
|
|
local decoration_h_without_scrollbars = window.DecoOuterSizeY1 + window.DecoOuterSizeY2 - window.ScrollbarSizes.y
|
|
local size_pad = ImVec2(window.WindowPadding.x * 2, window.WindowPadding.y * 2)
|
|
local size_desired = ImVec2()
|
|
size_desired.x = (bit32.band(axis_mask, 1) ~= 0) and (size_contents.x + size_pad.x + decoration_w_without_scrollbars) or window.Size.x
|
|
size_desired.y = (bit32.band(axis_mask, 2) ~= 0) and (size_contents.y + size_pad.y + decoration_h_without_scrollbars) or window.Size.y
|
|
|
|
-- Determine maximum window size
|
|
-- Child windows are laid within their parent (unless they are also popups/menus) and thus have no restriction
|
|
local size_max = ImVec2(FLT_MAX, FLT_MAX)
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0 or bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
if not window.ViewportOwned then
|
|
size_max = ImGui.GetMainViewport().WorkSize - style.DisplaySafeAreaPadding * 2.0
|
|
end
|
|
local monitor_idx = window.ViewportAllowPlatformMonitorExtend
|
|
if monitor_idx >= 1 and monitor_idx <= g.PlatformIO.Monitors.Size then
|
|
size_max = g.PlatformIO.Monitors.Data[monitor_idx].WorkSize - style.DisplaySafeAreaPadding * 2.0
|
|
end
|
|
end
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Tooltip) ~= 0 then
|
|
-- Tooltip always resize (up to maximum size)
|
|
return ImMin(size_desired, size_max)
|
|
else
|
|
local size_min = CalcWindowMinSize(window)
|
|
local size_auto_fit = ImClamp(size_desired, ImMin(size_min, size_max), size_max)
|
|
|
|
-- When the window cannot fit all contents (either because of constraints, either because screen is too small),
|
|
-- we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding.
|
|
local size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit)
|
|
local size_contents_for_scrollbar_x = (bit32.band(axis_mask, 1) ~= 0) and size_contents.x or window.ContentSize.x -- See #9352. In theory this should use same logic as `window.ScrollbarY = ...` codepath in Begin(). Needs some plumbling.
|
|
local size_contents_for_scrollbar_y = (bit32.band(axis_mask, 2) ~= 0) and size_contents.y or window.ContentSize.y
|
|
local will_have_scrollbar_x = (size_auto_fit_after_constraint.x < size_contents_for_scrollbar_x + size_pad.x + decoration_w_without_scrollbars and bit32.band(window.Flags, ImGuiWindowFlags.NoScrollbar) == 0 and bit32.band(window.Flags, ImGuiWindowFlags.HorizontalScrollbar) ~= 0) or bit32.band(window.Flags, ImGuiWindowFlags.AlwaysHorizontalScrollbar) ~= 0
|
|
local will_have_scrollbar_y = (size_auto_fit_after_constraint.y < size_contents_for_scrollbar_y + size_pad.y + decoration_h_without_scrollbars and bit32.band(window.Flags, ImGuiWindowFlags.NoScrollbar) == 0) or bit32.band(window.Flags, ImGuiWindowFlags.AlwaysVerticalScrollbar) ~= 0
|
|
if will_have_scrollbar_x then
|
|
size_auto_fit.y = size_auto_fit.y + style.ScrollbarSize
|
|
end
|
|
if will_have_scrollbar_y then
|
|
size_auto_fit.x = size_auto_fit.x + style.ScrollbarSize
|
|
end
|
|
return size_auto_fit
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return ImVec2
|
|
function ImGui.CalcWindowNextAutoFitSize(window)
|
|
local size_contents_current = ImVec2()
|
|
local size_contents_ideal = ImVec2()
|
|
CalcWindowContentSizes(window, size_contents_current, size_contents_ideal)
|
|
local size_auto_fit = CalcWindowAutoFitSize(window, size_contents_ideal, -1)
|
|
local size_final = CalcWindowSizeAfterConstraint(window, size_auto_fit)
|
|
return size_final
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return ImGuiCol
|
|
local function GetWindowBgColorIdx(window)
|
|
if bit32.band(window.Flags, bit32.bor(ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.Popup)) ~= 0 then
|
|
return ImGuiCol.PopupBg
|
|
elseif bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 and not window.DockIsActive then
|
|
return ImGuiCol.ChildBg
|
|
else
|
|
return ImGuiCol.WindowBg
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param corner_target_arg ImVec2
|
|
--- @param corner_norm ImVec2
|
|
--- @return ImVec2, ImVec2
|
|
--- @nodiscard
|
|
local function CalcResizePosSizeFromAnyCorner(window, corner_target_arg, corner_norm)
|
|
local corner_target = ImVec2()
|
|
ImVec2_Copy(corner_target, corner_target_arg)
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then -- Clamp resizing of childs within parent
|
|
local parent_window = window.ParentWindow
|
|
local parent_flags = parent_window.Flags
|
|
|
|
local limit_rect = ImRect()
|
|
ImRect_Copy(limit_rect, parent_window.InnerRect)
|
|
limit_rect:Expand(ImVec2(-ImMax(parent_window.WindowPadding.x, parent_window.WindowBorderSize), -ImMax(parent_window.WindowPadding.y, parent_window.WindowBorderSize)))
|
|
|
|
if (bit32.band(parent_flags, bit32.bor(ImGuiWindowFlags.HorizontalScrollbar, ImGuiWindowFlags.AlwaysHorizontalScrollbar)) == 0) or (bit32.band(parent_flags, ImGuiWindowFlags.NoScrollbar) ~= 0) then
|
|
corner_target.x = ImClamp(corner_target.x, limit_rect.Min.x, limit_rect.Max.x)
|
|
end
|
|
if bit32.band(parent_flags, ImGuiWindowFlags.NoScrollbar) ~= 0 then
|
|
corner_target.y = ImClamp(corner_target.y, limit_rect.Min.y, limit_rect.Max.y)
|
|
end
|
|
end
|
|
local pos_min = ImLerp(corner_target, window.Pos, corner_norm) -- Expected window upper-left
|
|
local pos_max = ImLerp(window.Pos + window.Size, corner_target, corner_norm) -- Expected window lower-right
|
|
local size_expected = pos_max - pos_min
|
|
local size_constrained = CalcWindowSizeAfterConstraint(window, size_expected)
|
|
|
|
local out_pos = pos_min
|
|
if corner_norm.x == 0.0 then
|
|
out_pos.x = out_pos.x - (size_constrained.x - size_expected.x)
|
|
end
|
|
if corner_norm.y == 0.0 then
|
|
out_pos.y = out_pos.y - (size_constrained.y - size_expected.y)
|
|
end
|
|
|
|
return out_pos, size_constrained
|
|
end
|
|
|
|
-- Handle resize for: Resize Grips, Borders, Gamepad
|
|
-- Return true when using auto-fit (double-click on resize grip)
|
|
-- Returns (ret_auto_fit_mask, border_hovered, border_held)
|
|
--- @param window ImGuiWindow
|
|
--- @param resize_grip_count int
|
|
--- @param resize_grip_col table
|
|
--- @param visibility_rect ImRect
|
|
--- @return int, int, int
|
|
local function UpdateWindowManualResize(window, resize_grip_count, resize_grip_col, visibility_rect)
|
|
local g = GImGui
|
|
local flags = window.Flags
|
|
|
|
if (bit32.band(flags, ImGuiWindowFlags.NoResize) ~= 0 or window.AutoFitFramesX > 0 or window.AutoFitFramesY > 0) then
|
|
return 0, -1, -1
|
|
end
|
|
if (bit32.band(flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0 and bit32.band(window.ChildFlags, bit32.bor(ImGuiChildFlags.ResizeX, ImGuiChildFlags.ResizeY)) == 0) then
|
|
return 0, -1, -1
|
|
end
|
|
if window.WasActive == false then -- Early out to avoid running this code for e.g. a hidden implicit/fallback Debug window.
|
|
return 0, -1, -1
|
|
end
|
|
|
|
local border_hovered = -1
|
|
local border_held = -1
|
|
|
|
local ret_auto_fit_mask = 0x00
|
|
local grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.35, g.Style.WindowRounding + 1.0 + g.FontSize * 0.2))
|
|
local grip_hover_inner_size = (resize_grip_count > 0) and IM_TRUNC(grip_draw_size * 0.75) or 0.0
|
|
local grip_hover_outer_size = g.WindowsBorderHoverPadding + 1
|
|
|
|
ImGui.PushID("#RESIZE")
|
|
|
|
local pos_target = ImVec2(FLT_MAX, FLT_MAX)
|
|
local size_target = ImVec2(FLT_MAX, FLT_MAX)
|
|
|
|
-- Clip mouse interaction rectangles within the viewport rectangle (in practice the narrowing is going to happen most of the time).
|
|
-- - Not narrowing would mostly benefit the situation where OS windows _without_ decoration have a threshold for hovering when outside their limits.
|
|
-- This is however not the case with current backends under Win32, but a custom borderless window implementation would benefit from it.
|
|
-- - When decoration are enabled we typically benefit from that distance, but then our resize elements would be conflicting with OS resize elements, so we also narrow.
|
|
-- - Note that we are unable to tell if the platform setup allows hovering with a distance threshold (on Win32, decorated window have such threshold).
|
|
-- We only clip interaction so we overwrite window.ClipRect, cannot call PushClipRect() yet as DrawList is not yet setup.
|
|
local clip_with_viewport_rect = bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.HasMouseHoveredViewport) == 0 or g.IO.MouseHoveredViewport ~= window.ViewportId or bit32.band(window.Viewport.Flags, ImGuiViewportFlags.NoDecoration) == 0
|
|
if (clip_with_viewport_rect) then
|
|
ImRect_Copy(window.ClipRect, window.Viewport:GetMainRect())
|
|
end
|
|
|
|
local clamp_rect = ImRect()
|
|
ImRect_Copy(clamp_rect, visibility_rect)
|
|
|
|
local window_move_from_title_bar = (bit32.band(window.BgClickFlags, ImGuiWindowBgClickFlags.Move) == 0) and (bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0)
|
|
if window_move_from_title_bar then
|
|
clamp_rect.Min.y = clamp_rect.Min.y - window.TitleBarHeight
|
|
end
|
|
|
|
for resize_grip_n = 0, resize_grip_count - 1 do
|
|
local def = ImGuiResizeGripDef[resize_grip_n + 1]
|
|
local corner_pos = def.CornerPosN
|
|
local inner_dir = def.InnerDir
|
|
|
|
local corner = ImVec2(window.Pos.x + corner_pos.x * window.Size.x, window.Pos.y + corner_pos.y * window.Size.y)
|
|
|
|
-- Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window
|
|
local resize_rect = ImRect(corner - inner_dir * grip_hover_outer_size, corner + inner_dir * grip_hover_inner_size)
|
|
|
|
if resize_rect.Min.x > resize_rect.Max.x then resize_rect.Min.x, resize_rect.Max.x = resize_rect.Max.x, resize_rect.Min.x end
|
|
if resize_rect.Min.y > resize_rect.Max.y then resize_rect.Min.y, resize_rect.Max.y = resize_rect.Max.y, resize_rect.Min.y end
|
|
|
|
local resize_grip_id = window:GetID(resize_grip_n) -- == GetWindowResizeCornerID()
|
|
|
|
ImGui.ItemAdd(resize_rect, resize_grip_id, nil, ImGuiItemFlags.NoNav)
|
|
local _, hovered, held = ImGui.ButtonBehavior(resize_rect, resize_grip_id, bit32.bor(ImGuiButtonFlags.FlattenChildren, ImGuiButtonFlags.NoNavFocus))
|
|
-- GetForegroundDrawList(window):AddRect(resize_rect.Min, resize_rect.Max, IM_COL32(255, 255, 0, 255))
|
|
|
|
if hovered or held then
|
|
ImGui.SetMouseCursor((bit32.band(resize_grip_n, 1) ~= 0) and ImGuiMouseCursor.ResizeNESW or ImGuiMouseCursor.ResizeNWSE)
|
|
end
|
|
|
|
if held and g.IO.MouseDoubleClicked[0] then
|
|
-- Auto-fit when double-clicking
|
|
local size_auto_fit = CalcWindowAutoFitSize(window, window.ContentSizeIdeal, -1)
|
|
size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit)
|
|
ret_auto_fit_mask = 0x03 -- Both axes
|
|
ImGui.ClearActiveID()
|
|
elseif held then
|
|
-- Resize from any of the four corners
|
|
-- We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position
|
|
local clamp_min = ImVec2((corner_pos.x == 1.0) and clamp_rect.Min.x or -FLT_MAX, (corner_pos.y == 1.0) and clamp_rect.Min.y or -FLT_MAX)
|
|
local clamp_max = ImVec2((corner_pos.x == 0.0) and clamp_rect.Max.x or FLT_MAX, (corner_pos.y == 0.0) and clamp_rect.Max.y or FLT_MAX)
|
|
local corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN) -- Corner of the window corresponding to our corner grip
|
|
|
|
corner_target = ImClamp(corner_target, clamp_min, clamp_max)
|
|
|
|
pos_target, size_target = CalcResizePosSizeFromAnyCorner(window, corner_target, corner_pos)
|
|
end
|
|
|
|
-- Only lower-left grip is visible before hovering/activating
|
|
local resize_grip_visible = held or hovered or (resize_grip_n == 0 and bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0)
|
|
if resize_grip_visible then
|
|
local color
|
|
if held then
|
|
color = ImGui.GetColorU32(ImGuiCol.ResizeGripActive)
|
|
else
|
|
if hovered then
|
|
color = ImGui.GetColorU32(ImGuiCol.ResizeGripHovered)
|
|
else
|
|
color = ImGui.GetColorU32(ImGuiCol.ResizeGrip)
|
|
end
|
|
end
|
|
resize_grip_col[resize_grip_n + 1] = color
|
|
end
|
|
end
|
|
|
|
local resize_border_mask = 0x00
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
local mask_x = (bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeX) ~= 0) and 0x02 or 0
|
|
local mask_y = (bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeY) ~= 0) and 0x08 or 0
|
|
resize_border_mask = bit32.bor(mask_x, mask_y)
|
|
else
|
|
resize_border_mask = g.IO.ConfigWindowsResizeFromEdges and 0x0F or 0x00
|
|
end
|
|
|
|
for border_n = 0, 3 do
|
|
repeat
|
|
if bit32.band(resize_border_mask, bit32.lshift(1, border_n)) == 0 then
|
|
break
|
|
end
|
|
|
|
local def = ImGuiResizeBorderDef[border_n + 1]
|
|
local axis
|
|
if border_n == ImGuiDir.Left or border_n == ImGuiDir.Right then
|
|
axis = ImGuiAxis.X
|
|
else
|
|
axis = ImGuiAxis.Y
|
|
end
|
|
|
|
local border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, g.WindowsBorderHoverPadding)
|
|
local border_id = window:GetID(border_n + 4) -- == GetWindowResizeBorderID()
|
|
ImGui.ItemAdd(border_rect, border_id, nil, ImGuiItemFlags.NoNav)
|
|
local _, hovered, held = ImGui.ButtonBehavior(border_rect, border_id, bit32.bor(ImGuiButtonFlags.FlattenChildren, ImGuiButtonFlags.NoNavFocus))
|
|
-- GetForegroundDrawList(window):AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255))
|
|
|
|
if hovered and g.HoveredIdTimer <= WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER then
|
|
hovered = false
|
|
end
|
|
if hovered or held then
|
|
ImGui.SetMouseCursor((axis == ImGuiAxis.X) and ImGuiMouseCursor.ResizeEW or ImGuiMouseCursor.ResizeNS)
|
|
end
|
|
if held and g.IO.MouseDoubleClicked[0] then
|
|
-- Double-clicking bottom or right border auto-fit on this axis
|
|
-- FIXME: Support top and right borders: rework CalcResizePosSizeFromAnyCorner() to be reusable in both cases.
|
|
if border_n == 1 or border_n == 3 then -- Right and bottom border
|
|
local size_auto_fit = CalcWindowAutoFitSize(window, window.ContentSizeIdeal, bit32.lshift(1, axis - 1))
|
|
size_target[axis] = CalcWindowSizeAfterConstraint(window, size_auto_fit)[axis]
|
|
ret_auto_fit_mask = bit32.bor(ret_auto_fit_mask, bit32.lshift(1, axis - 1))
|
|
hovered = false
|
|
held = false -- So border doesn't show highlighted at new position
|
|
end
|
|
ImGui.ClearActiveID()
|
|
elseif held then
|
|
-- Switch to relative resizing mode when border geometry moved (e.g. resizing a child altering parent scroll), in order to avoid resizing feedback loop.
|
|
-- Currently only using relative mode on resizable child windows, as the problem to solve is more likely noticeable for them, but could apply for all windows eventually.
|
|
-- FIXME: May want to generalize this idiom at lower-level, so more widgets can use it!
|
|
local just_scrolled_manually_while_resizing = (g.WheelingWindow ~= nil and g.WheelingWindowScrolledFrame == g.FrameCount and ImGui.IsWindowChildOf(window, g.WheelingWindow, false, true))
|
|
if g.ActiveIdIsJustActivated or just_scrolled_manually_while_resizing then
|
|
g.WindowResizeBorderExpectedRect = border_rect
|
|
g.WindowResizeRelativeMode = false
|
|
end
|
|
|
|
if (bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0) and g.WindowResizeBorderExpectedRect ~= border_rect then
|
|
g.WindowResizeRelativeMode = true
|
|
end
|
|
|
|
local border_curr = window.Pos + ImMin(def.SegmentN1, def.SegmentN2) * window.Size
|
|
local border_target_rel_mode_for_axis
|
|
local border_target_abs_mode_for_axis
|
|
border_target_rel_mode_for_axis = border_curr[axis] + g.IO.MouseDelta[axis]
|
|
border_target_abs_mode_for_axis = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + g.WindowsBorderHoverPadding -- Match ButtonBehavior() padding above.
|
|
|
|
-- Use absolute mode position
|
|
local border_target = ImVec2()
|
|
ImVec2_Copy(border_target, window.Pos)
|
|
border_target[axis] = border_target_abs_mode_for_axis
|
|
|
|
-- Use relative mode target for child window, ignore resize when moving back toward the ideal absolute position.
|
|
local ignore_resize = false
|
|
if g.WindowResizeRelativeMode then
|
|
-- GetForegroundDrawList():AddText(GetMainViewport().WorkPos, IM_COL32_WHITE, "Relative Mode")
|
|
border_target[axis] = border_target_rel_mode_for_axis
|
|
if g.IO.MouseDelta[axis] == 0.0 or ((g.IO.MouseDelta[axis] > 0.0) == (border_target_rel_mode_for_axis > border_target_abs_mode_for_axis)) then
|
|
ignore_resize = true
|
|
end
|
|
end
|
|
|
|
-- Clamp, apply
|
|
local clamp_min = ImVec2((border_n == ImGuiDir.Right) and clamp_rect.Min.x or -FLT_MAX, (border_n == ImGuiDir.Down or (border_n == ImGuiDir.Up and window_move_from_title_bar)) and clamp_rect.Min.y or -FLT_MAX)
|
|
local clamp_max = ImVec2((border_n == ImGuiDir.Left) and clamp_rect.Max.x or FLT_MAX, (border_n == ImGuiDir.Up) and clamp_rect.Max.y or FLT_MAX)
|
|
border_target = ImClamp(border_target, clamp_min, clamp_max)
|
|
|
|
if not ignore_resize then
|
|
pos_target, size_target = CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2))
|
|
end
|
|
end
|
|
if hovered then
|
|
border_hovered = border_n
|
|
end
|
|
if held then
|
|
border_held = border_n
|
|
end
|
|
|
|
until true
|
|
end
|
|
ImGui.PopID()
|
|
|
|
-- Restore nav layer
|
|
window.DC.NavLayerCurrent = ImGuiNavLayer.Main
|
|
|
|
-- Apply back modified position/size to window
|
|
if size_target.x ~= FLT_MAX and (window.Size.x ~= size_target.x or window.SizeFull.x ~= size_target.x) then
|
|
window.Size.x = size_target.x
|
|
window.SizeFull.x = size_target.x
|
|
end
|
|
|
|
if size_target.y ~= FLT_MAX and (window.Size.y ~= size_target.y or window.SizeFull.y ~= size_target.y) then
|
|
window.Size.y = size_target.y
|
|
window.SizeFull.y = size_target.y
|
|
end
|
|
|
|
if pos_target.x ~= FLT_MAX and window.Pos.x ~= ImFloor(pos_target.x) then
|
|
window.Pos.x = ImFloor(pos_target.x)
|
|
end
|
|
|
|
if pos_target.y ~= FLT_MAX and window.Pos.y ~= ImFloor(pos_target.y) then
|
|
window.Pos.y = ImFloor(pos_target.y)
|
|
end
|
|
|
|
-- Recalculate next expected border expected coordinates
|
|
if border_held ~= -1 then
|
|
g.WindowResizeBorderExpectedRect = GetResizeBorderRect(window, border_held, grip_hover_inner_size, g.WindowsBorderHoverPadding)
|
|
end
|
|
|
|
return ret_auto_fit_mask, border_hovered, border_held
|
|
end
|
|
|
|
--- @param pos ImVec2
|
|
--- @param wrap_pos_x float
|
|
--- @return float
|
|
function ImGui.CalcWrapWidthForPos(pos, wrap_pos_x)
|
|
if wrap_pos_x < 0.0 then
|
|
return 0.0
|
|
end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if wrap_pos_x == 0.0 then
|
|
-- We could decide to setup a default wrapping max point for auto-resizing windows,
|
|
-- or have auto-wrap (with unspecified wrapping pos) behave as a ContentSize extending function?
|
|
-- if (window.Hidden and (window.Flags & ImGuiWindowFlags.AlwaysAutoResize))
|
|
-- wrap_pos_x = ImMax(window.WorkRect.Min.x + g.FontSize * 10.0, window.WorkRect.Max.x)
|
|
-- else
|
|
wrap_pos_x = window.WorkRect.Max.x
|
|
elseif wrap_pos_x > 0.0 then
|
|
wrap_pos_x = wrap_pos_x + window.Pos.x - window.Scroll.x -- wrap_pos_x is provided is window local space
|
|
end
|
|
|
|
return ImMax(wrap_pos_x - pos.x, 1.0)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] RENDER HELPERS
|
|
-- Some of those (internal) functions are currently quite a legacy mess - their signature and behavior will change,
|
|
-- we need a nicer separation between low-level functions and high-level functions relying on the ImGui context.
|
|
-- Also see imgui_draw.cpp for some more which have been reworked to not rely on ImGui:: context.
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param text ImString
|
|
--- @param text_end int?
|
|
--- @return int # Exclusive upper bound
|
|
@native
|
|
function ImGui.FindRenderedTextEnd(text, text_end)
|
|
local text_display_end = 1
|
|
local text_len = #text
|
|
if not text_end then
|
|
text_end = text_len + 1
|
|
end
|
|
|
|
text_display_end = ImMemchr(text, "##", 1) or text_end
|
|
text_display_end = ImMin(text_display_end, text_end)
|
|
|
|
return text_display_end
|
|
end
|
|
|
|
-- Internal ImGui functions to render text
|
|
-- RenderText***() functions calls ImDrawList::AddText() calls ImBitmapFont::RenderText()
|
|
--- @param pos ImVec2
|
|
--- @param text string
|
|
--- @param text_begin int?
|
|
--- @param text_end int?
|
|
--- @param hide_text_after_hash bool? # true by default
|
|
@native
|
|
function ImGui.RenderText(pos, text, text_begin, text_end, hide_text_after_hash)
|
|
if text_begin == nil then text_begin = 1 end
|
|
if hide_text_after_hash == nil then hide_text_after_hash = true end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
-- Hide anything after a '##' string
|
|
local text_display_end
|
|
if hide_text_after_hash then
|
|
text_display_end = ImGui.FindRenderedTextEnd(text, text_end)
|
|
else
|
|
if text_end == nil then
|
|
text_end = #text + 1 -- FIXME-OPT (not reached by our internal calls)
|
|
end
|
|
text_display_end = text_end
|
|
end
|
|
|
|
if text ~= "" and text_display_end > 1 then
|
|
window.DrawList:AddText(g.Font, g.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), text, text_begin, text_display_end, 0.0)
|
|
if g.LogEnabled then
|
|
-- ImGui.LogRenderedText(&pos, text, text_display_end);
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param pos ImVec2
|
|
--- @param text string
|
|
--- @param text_end int?
|
|
--- @param wrap_width float
|
|
function ImGui.RenderTextWrapped(pos, text, text_end, wrap_width)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
if text_end == nil then
|
|
text_end = #text + 1 -- FIXME-OPT (not reached by our internal calls)
|
|
end
|
|
|
|
if text ~= "" then
|
|
window.DrawList:AddText(g.Font, g.FontSize, pos, ImGui.GetColorU32(ImGuiCol.Text), text, 1, text_end, wrap_width)
|
|
end
|
|
end
|
|
|
|
-- Default clip_rect uses (pos_min,pos_max)
|
|
-- Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges)
|
|
-- FIXME-OPT: Since we have or calculate text_size we could coarse clip whole block immediately, especially for text above draw_list->DrawList.
|
|
-- Effectively as this is called from widget doing their own coarse clipping it's not very valuable presently. Next time function will take
|
|
-- better advantage of the render function taking size into account for coarse clipping.
|
|
-- DOES NOT CALL LogRenderedText(), unlike RenderTextClipped!!!
|
|
--- @param draw_list ImDrawList
|
|
--- @param pos_min ImVec2
|
|
--- @param pos_max ImVec2
|
|
--- @param text string
|
|
--- @param text_begin int
|
|
--- @param text_display_end int
|
|
--- @param text_size_if_known? ImVec2
|
|
--- @param align? ImVec2
|
|
--- @param clip_rect? ImRect
|
|
@native
|
|
function ImGui.RenderTextClippedEx(draw_list, pos_min, pos_max, text, text_begin, text_display_end, text_size_if_known, align, clip_rect)
|
|
if not align then align = ImVec2(0, 0) end
|
|
|
|
-- Perform CPU side clipping for single clipped element to avoid using scissor state
|
|
local pos = ImVec2()
|
|
ImVec2_Copy(pos, pos_min)
|
|
|
|
local text_size = text_size_if_known or ImGui.CalcTextSize(text, text_display_end, false, 0.0)
|
|
|
|
local clip_min = clip_rect and clip_rect.Min or pos_min
|
|
local clip_max = clip_rect and clip_rect.Max or pos_max
|
|
local need_clipping = (pos.x + text_size.x >= clip_max.x) or (pos.y + text_size.y >= clip_max.y)
|
|
if (clip_rect) then -- If we had no explicit clipping rectangle then pos==clip_min
|
|
need_clipping = need_clipping or ((pos.x < clip_min.x) or (pos.y < clip_min.y))
|
|
end
|
|
|
|
-- Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment.
|
|
if (align.x > 0.0) then pos.x = ImMax(pos.x, pos.x + (pos_max.x - pos.x - text_size.x) * align.x) end
|
|
if (align.y > 0.0) then pos.y = ImMax(pos.y, pos.y + (pos_max.y - pos.y - text_size.y) * align.y) end
|
|
|
|
-- Render
|
|
local g = GImGui
|
|
if (need_clipping) then
|
|
local fine_clip_rect = ImVec4(clip_min.x, clip_min.y, clip_max.x, clip_max.y)
|
|
draw_list:AddText(nil, 0.0, pos, ImGui.GetColorU32(ImGuiCol.Text), text, text_begin, text_display_end, 0.0, fine_clip_rect)
|
|
else
|
|
draw_list:AddText(nil, 0.0, pos, ImGui.GetColorU32(ImGuiCol.Text), text, text_begin, text_display_end, 0.0, nil)
|
|
end
|
|
end
|
|
|
|
--- @param pos_min ImVec2
|
|
--- @param pos_max ImVec2
|
|
--- @param text string
|
|
--- @param text_end? int
|
|
--- @param text_size_if_known? ImVec2
|
|
--- @param align? ImVec2
|
|
--- @param clip_rect? ImRect
|
|
function ImGui.RenderTextClipped(pos_min, pos_max, text, text_end, text_size_if_known, align, clip_rect)
|
|
if not align then align = ImVec2(0, 0) end
|
|
|
|
-- Hide anything after a '##' string
|
|
local text_display_end = ImGui.FindRenderedTextEnd(text, text_end)
|
|
local text_len = text_display_end - 1
|
|
|
|
if text_len == 0 then
|
|
return
|
|
end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
ImGui.RenderTextClippedEx(window.DrawList, pos_min, pos_max, text, 1, text_display_end, text_size_if_known, align, clip_rect)
|
|
-- if (g.LogEnabled)
|
|
-- ImGui.LogRenderedText(&pos_min, text, text_display_end);
|
|
end
|
|
|
|
--- @param bb ImRect
|
|
--- @param id ImGuiID
|
|
--- @param flags? ImGuiNavRenderCursorFlags
|
|
--- @param rounding? float
|
|
@native
|
|
function ImGui.RenderNavCursor(bb, id, flags, rounding)
|
|
if flags == nil then flags = ImGuiNavRenderCursorFlags.None end
|
|
if rounding == nil then rounding = -1.0 end
|
|
|
|
local g = GImGui
|
|
if id ~= g.NavId then
|
|
return
|
|
end
|
|
if not g.NavCursorVisible and bit32.band(flags, ImGuiNavRenderCursorFlags.AlwaysDraw) == 0 then
|
|
return
|
|
end
|
|
if id == g.LastItemData.ID and bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.NoNav) ~= 0 then
|
|
return
|
|
end
|
|
|
|
-- We don't early out on 'window.Flags & ImGuiWindowFlags.NoNavInputs' because it would be inconsistent with
|
|
-- other code directly checking NavCursorVisible. Instead we aim for NavCursorVisible to always be false.
|
|
local window = g.CurrentWindow
|
|
if window.DC.NavHideHighlightOneFrame then
|
|
return
|
|
end
|
|
|
|
if rounding < 0.0 then
|
|
rounding = g.Style.FrameRounding
|
|
end
|
|
|
|
local display_rect = ImRect()
|
|
ImRect_Copy(display_rect, bb)
|
|
display_rect:ClipWith(window.ClipRect)
|
|
local scale_factor = ImGui.GetScale() -- FIXME-DPI
|
|
local thickness = ImTrunc(ImMax(2.0, 1.5 * scale_factor))
|
|
if bit32.band(flags, ImGuiNavRenderCursorFlags.Compact) ~= 0 then
|
|
window.DrawList:AddRect(display_rect.Min, display_rect.Max, ImGui.GetColorU32(ImGuiCol.NavCursor), rounding, thickness)
|
|
else
|
|
local distance = ImTrunc(3.0 + thickness * 0.5)
|
|
display_rect:Expand(ImVec2(distance, distance))
|
|
local fully_visible = window.ClipRect:Contains(display_rect)
|
|
if not fully_visible then
|
|
window.DrawList:PushClipRect(display_rect.Min, display_rect.Max)
|
|
end
|
|
window.DrawList:AddRect(display_rect.Min, display_rect.Max, ImGui.GetColorU32(ImGuiCol.NavCursor), rounding, thickness)
|
|
if not fully_visible then
|
|
window.DrawList:PopClipRect()
|
|
end
|
|
end
|
|
end
|
|
|
|
do --[[ImGui.RenderMouseCursor]]
|
|
|
|
local offset = ImVec2()
|
|
local size = ImVec2()
|
|
local uv_border = {ImVec2(), ImVec2()}
|
|
local uv_fill = {ImVec2(), ImVec2()}
|
|
|
|
--- @param base_pos ImVec2
|
|
--- @param base_scale float
|
|
--- @param mouse_cursor ImGuiMouseCursor
|
|
--- @param col_fill ImU32
|
|
--- @param col_border ImU32
|
|
--- @param col_shadow ImU32
|
|
function ImGui.RenderMouseCursor(base_pos, base_scale, mouse_cursor, col_fill, col_border, col_shadow)
|
|
local g = GImGui
|
|
if mouse_cursor <= ImGuiMouseCursor.None or mouse_cursor >= ImGuiMouseCursor.COUNT then -- We intentionally accept out of bound values.
|
|
mouse_cursor = ImGuiMouseCursor.Arrow
|
|
end
|
|
local font_atlas = g.DrawListSharedData.FontAtlas
|
|
|
|
for _, viewport in g.Viewports:iter() do
|
|
repeat
|
|
-- We scale cursor with current viewport/monitor, however Windows 10 for its own hardware cursor seems to be using a different scale factor.
|
|
if not ImFontAtlasGetMouseCursorTexData(font_atlas, mouse_cursor, offset, size, uv_border, uv_fill) then
|
|
break
|
|
end
|
|
|
|
local pos = base_pos - offset
|
|
local scale = base_scale * viewport.DpiScale
|
|
if not viewport:GetMainRect():Overlaps(ImRect(pos, pos + ImVec2(size.x + 2, size.y + 2) * scale)) then
|
|
break
|
|
end
|
|
|
|
local draw_list = ImGui.GetForegroundDrawList(viewport)
|
|
local tex_ref = font_atlas.TexRef
|
|
draw_list:PushTexture(tex_ref);
|
|
draw_list:AddImage(tex_ref, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv_fill[1], uv_fill[2], col_shadow)
|
|
draw_list:AddImage(tex_ref, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv_fill[1], uv_fill[2], col_shadow)
|
|
draw_list:AddImage(tex_ref, pos, pos + size * scale, uv_fill[1], uv_fill[2], col_border)
|
|
draw_list:AddImage(tex_ref, pos, pos + size * scale, uv_border[1], uv_border[2], col_fill)
|
|
if mouse_cursor == ImGuiMouseCursor.Wait or mouse_cursor == ImGuiMouseCursor.Progress then
|
|
local a_min = ImFmod(g.Time * 5.0, 2.0 * IM_PI)
|
|
local a_max = a_min + IM_PI * 1.65
|
|
draw_list:PathArcTo(pos + ImVec2(14, -1) * scale, 6.0 * scale, a_min, a_max)
|
|
draw_list:PathStroke(col_fill, 3.0 * scale)
|
|
end
|
|
draw_list:PopTexture()
|
|
|
|
until true
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- Another overly complex function until we reorganize everything into a nice all-in-one helper.
|
|
-- This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) from 'ellipsis_max_x' which may be beyond it.
|
|
-- This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move.
|
|
-- (BREAKING) On 2025/04/16 we removed the 'float clip_max_x' parameters which was preceding 'float ellipsis_max' and was the same value for 99% of users.
|
|
--- @param draw_list ImDrawList
|
|
--- @param pos_min ImVec2
|
|
--- @param pos_max ImVec2
|
|
--- @param ellipsis_max_x float
|
|
--- @param text string
|
|
--- @param text_end_full? int
|
|
--- @param text_size_if_known? ImVec2
|
|
function ImGui.RenderTextEllipsis(draw_list, pos_min, pos_max, ellipsis_max_x, text, text_end_full, text_size_if_known)
|
|
local g = GImGui
|
|
|
|
if text_end_full == nil then
|
|
text_end_full = ImGui.FindRenderedTextEnd(text)
|
|
end
|
|
|
|
local text_size
|
|
if text_size_if_known then
|
|
text_size = ImVec2()
|
|
ImVec2_Copy(text_size, text_size_if_known)
|
|
else
|
|
text_size = ImGui.CalcTextSize(text, text_end_full, false, 0.0)
|
|
end
|
|
|
|
-- draw_list:AddLineV(pos_max.x, pos_min.y - 4, pos_max.y + 6, IM_COL32(0, 0, 255, 255))
|
|
-- draw_list:AddLineV(ellipsis_max_x, pos_min.y - 2, pos_max.y + 3, IM_COL32(0, 255, 0, 255))
|
|
|
|
-- FIXME: We could technically remove (last_glyph->AdvanceX - last_glyph->X1) from text_size.x here and save a few pixels.
|
|
if text_size.x > pos_max.x - pos_min.x then
|
|
-- Hello wo...
|
|
-- | | |
|
|
-- min max ellipsis_max
|
|
-- <-> this is generally some padding value
|
|
|
|
local font = draw_list._Data.Font
|
|
local font_size = draw_list._Data.FontSize
|
|
local font_scale = draw_list._Data.FontScale
|
|
local text_end_ellipsis = nil
|
|
local baked = font:GetFontBaked(font_size)
|
|
local ellipsis_width = baked:GetCharAdvance(font.EllipsisChar) * font_scale
|
|
|
|
-- We can now claim the space between pos_max.x and ellipsis_max.x
|
|
local text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_width) - pos_min.x, 1.0)
|
|
local text_size_clipped
|
|
text_size_clipped, text_end_ellipsis = font:CalcTextSizeA(font_size, text_avail_width, 0.0, text, 1, text_end_full)
|
|
local text_size_clipped_x = text_size_clipped.x
|
|
|
|
-- Render text, render ellipsis
|
|
ImGui.RenderTextClippedEx(draw_list, pos_min, pos_max, text, 1, text_end_ellipsis, text_size, ImVec2(0.0, 0.0))
|
|
local cpu_fine_clip_rect = ImVec4(pos_min.x, pos_min.y, pos_max.x, pos_max.y)
|
|
local ellipsis_pos = ImTrunc(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y))
|
|
font:RenderChar(draw_list, font_size, ellipsis_pos, ImGui.GetColorU32(ImGuiCol.Text), font.EllipsisChar, cpu_fine_clip_rect)
|
|
else
|
|
ImGui.RenderTextClippedEx(draw_list, pos_min, pos_max, text, 1, text_end_full, text_size, ImVec2(0.0, 0.0))
|
|
end
|
|
|
|
if g.LogEnabled then
|
|
ImGui.LogRenderedText(pos_min, text, text_end_full)
|
|
end
|
|
end
|
|
|
|
-- Render a rectangle shaped with optional rounding and borders
|
|
--- @param p_min ImVec2
|
|
--- @param p_max ImVec2
|
|
--- @param fill_col ImU32
|
|
--- @param borders? bool
|
|
--- @param rounding? float
|
|
@native
|
|
function ImGui.RenderFrame(p_min, p_max, fill_col, borders, rounding)
|
|
if borders == nil then borders = true end
|
|
if rounding == nil then rounding = 0.0 end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
window.DrawList:AddRectFilled(p_min, p_max, fill_col, rounding)
|
|
|
|
local border_size = g.Style.FrameBorderSize
|
|
if borders and border_size > 0 then
|
|
window.DrawList:AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), ImGui.GetColorU32(ImGuiCol.BorderShadow), rounding, border_size, 0)
|
|
window.DrawList:AddRect(p_min, p_max, ImGui.GetColorU32(ImGuiCol.Border), rounding, border_size, 0)
|
|
end
|
|
end
|
|
|
|
--- @param p_min ImVec2
|
|
--- @param p_max ImVec2
|
|
--- @param rounding float
|
|
@native
|
|
function ImGui.RenderFrameBorder(p_min, p_max, rounding)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local border_size = g.Style.FrameBorderSize
|
|
if border_size > 0.0 then
|
|
window.DrawList:AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), ImGui.GetColorU32(ImGuiCol.BorderShadow), rounding, border_size, 0)
|
|
window.DrawList:AddRect(p_min, p_max, ImGui.GetColorU32(ImGuiCol.Border), rounding, border_size, 0)
|
|
end
|
|
end
|
|
|
|
--- @param bb ImRect
|
|
--- @param col ImU32
|
|
--- @param rounding float
|
|
function ImGui.RenderColorComponentMarker(bb, col, rounding)
|
|
if bb.Min.x + 1 >= bb.Max.x then
|
|
return
|
|
end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
ImGui.RenderRectFilledInRangeH(window.DrawList, bb, col, bb.Min.x, ImMin(bb.Min.x + g.Style.ColorMarkerSize, bb.Max.x), rounding)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param border_n int
|
|
--- @param border_col ImU32
|
|
--- @param border_size float
|
|
local function RenderWindowOuterSingleBorder(window, border_n, border_col, border_size)
|
|
local def = ImGuiResizeBorderDef[border_n + 1]
|
|
local rounding = window.WindowRounding
|
|
local border_r = GetResizeBorderRect(window, border_n, rounding, 0.0)
|
|
window.DrawList:PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5, 0.5) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25, def.OuterAngle)
|
|
window.DrawList:PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5, 0.5) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25)
|
|
window.DrawList:PathStroke(border_col, border_size)
|
|
end
|
|
|
|
local function RenderWindowOuterBorders(window)
|
|
local g = GImGui
|
|
local border_size = window.WindowBorderSize
|
|
local border_col = ImGui.GetColorU32(ImGuiCol.Border)
|
|
if border_size > 0.0 and (bit32.band(window.Flags, ImGuiWindowFlags.NoBackground) == 0) then
|
|
window.DrawList:AddRect(window.Pos, window.Pos + window.Size, border_col, window.WindowRounding, window.WindowBorderSize, 0)
|
|
elseif border_size > 0.0 then
|
|
if bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeX) ~= 0 then -- Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border.
|
|
RenderWindowOuterSingleBorder(window, 1, border_col, border_size)
|
|
end
|
|
if bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeY) ~= 0 then
|
|
RenderWindowOuterSingleBorder(window, 3, border_col, border_size)
|
|
end
|
|
end
|
|
|
|
if window.ResizeBorderHovered ~= -1 or window.ResizeBorderHeld ~= -1 then
|
|
local border_n = (window.ResizeBorderHeld ~= -1) and window.ResizeBorderHeld or window.ResizeBorderHovered
|
|
local border_col_resizing = ImGui.GetColorU32((window.ResizeBorderHeld ~= -1) and ImGuiCol.SeparatorActive or ImGuiCol.SeparatorHovered)
|
|
RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0, window.WindowBorderSize)) -- Thicker than usual
|
|
end
|
|
|
|
if g.Style.FrameBorderSize > 0 and (bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0) and not window.DockIsActive then
|
|
local y = window.Pos.y + window.TitleBarHeight - 1
|
|
window.DrawList:AddLineH(window.Pos.x + border_size * 0.5, window.Pos.x + window.Size.x - border_size * 0.5, y, border_col, g.Style.FrameBorderSize)
|
|
end
|
|
end
|
|
|
|
-- Draw background and borders
|
|
-- Draw and handle scrollbars
|
|
--- @param window ImGuiWindow
|
|
--- @param title_bar_rect ImRect
|
|
--- @param title_bar_is_highlight bool
|
|
--- @param handle_borders_and_resize_grips bool
|
|
--- @param resize_grip_col ImU32[]
|
|
--- @param resize_grip_draw_size float
|
|
local function RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_col, resize_grip_draw_size)
|
|
local g = GImGui
|
|
local style = g.Style
|
|
local flags = window.Flags
|
|
|
|
-- Draw window + handle manual resize
|
|
-- As we highlight the title bar when want_focus is set, multiple reappearing windows will have their title bar highlighted on their reappearing frame.
|
|
local window_rounding = window.WindowRounding
|
|
local window_border_size = window.WindowBorderSize
|
|
|
|
if window.Collapsed then
|
|
-- Title bar only
|
|
local backup_border_size = style.FrameBorderSize
|
|
g.Style.FrameBorderSize = window.WindowBorderSize
|
|
|
|
local title_bar_col = ImGui.GetColorU32((title_bar_is_highlight and g.NavCursorVisible) and ImGuiCol.TitleBgActive or ImGuiCol.TitleBgCollapsed)
|
|
ImGui.RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding)
|
|
g.Style.FrameBorderSize = backup_border_size
|
|
else
|
|
-- Window background
|
|
if bit32.band(flags, ImGuiWindowFlags.NoBackground) == 0 then
|
|
local bg_col = ImGui.GetColorU32(GetWindowBgColorIdx(window))
|
|
-- Adjust alpha. For docking
|
|
local override_alpha = false
|
|
local alpha = 1.0
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasBgAlpha) ~= 0 then
|
|
alpha = g.NextWindowData.BgAlphaVal
|
|
override_alpha = true
|
|
end
|
|
|
|
if override_alpha then
|
|
bg_col = bit32.bor(bit32.band(bg_col, bit32.bnot(IM_COL32_A_MASK)), bit32.lshift(IM_F32_TO_INT8_SAT(alpha), IM_COL32_A_SHIFT))
|
|
end
|
|
|
|
-- Render, for docked windows and host windows we ensure BG goes before decorations
|
|
if window.DockIsActive then window.DockNode.LastBgColor = bg_col end
|
|
if bit32.band(flags, ImGuiWindowFlags.DockNodeHost) ~= 0 then bg_col = 0 end
|
|
|
|
if bit32.band(bg_col, IM_COL32_A_MASK) ~= 0 then
|
|
local bg_rect = ImRect(window.Pos + ImVec2(0, window.TitleBarHeight), window.Pos + window.Size)
|
|
local bg_rounding_flags
|
|
if window.DockIsActive then
|
|
bg_rounding_flags = ImGui.CalcRoundingFlagsForRectInRect(bg_rect, window.DockNode.HostWindow:Rect(), 0.0)
|
|
else
|
|
bg_rounding_flags = (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and ImDrawFlags.RoundCornersAll or ImDrawFlags.RoundCornersBottom
|
|
end
|
|
local bg_draw_list = window.DockIsActive and window.DockNode.HostWindow.DrawList or window.DrawList
|
|
if window.DockIsActive then bg_draw_list:ChannelsSetCurrent(0) end
|
|
bg_draw_list:AddRectFilled(bg_rect.Min, bg_rect.Max, bg_col, window_rounding, bg_rounding_flags)
|
|
if window.DockIsActive then bg_draw_list:ChannelsSetCurrent(1) end
|
|
end
|
|
end
|
|
if window.DockIsActive then window.DockNode.IsBgDrawnThisFrame = true end
|
|
|
|
-- Title bar
|
|
-- (when docked, DockNode are drawing their own title bar. Individual windows however do NOT set the _NoTitleBar flag,
|
|
-- in order for their pos/size to be matching their undocking state.)
|
|
if bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0 and not window.DockIsActive then
|
|
local title_bar_col = ImGui.GetColorU32(title_bar_is_highlight and ImGuiCol.TitleBgActive or ImGuiCol.TitleBg)
|
|
window.DrawList:AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawFlags.RoundCornersTop)
|
|
end
|
|
|
|
-- Menu bar
|
|
if bit32.band(flags, ImGuiWindowFlags.MenuBar) ~= 0 then
|
|
local menu_bar_rect = window:MenuBarRect()
|
|
menu_bar_rect:ClipWith(window:Rect()) -- Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them.
|
|
window.DrawList:AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, ImGui.GetColorU32(ImGuiCol.MenuBarBg), (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and window_rounding or 0.0, ImDrawFlags.RoundCornersTop)
|
|
if style.FrameBorderSize > 0.0 and menu_bar_rect.Max.y < window.Pos.y + window.Size.y then
|
|
window.DrawList:AddLineH(menu_bar_rect.Min.x + window_border_size * 0.5, menu_bar_rect.Max.x - window_border_size * 0.5, menu_bar_rect.Max.y, ImGui.GetColorU32(ImGuiCol.Border), style.FrameBorderSize)
|
|
end
|
|
end
|
|
|
|
-- Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock
|
|
local node = window.DockNode
|
|
if window.DockIsActive and node:IsHiddenTabBar() and not node:IsNoTabBar() then
|
|
local unhide_sz_draw = ImTrunc(g.FontSize * 0.70)
|
|
local unhide_sz_hit = ImTrunc(g.FontSize * 0.55)
|
|
local p = node.Pos
|
|
local r = ImRect(p, p + ImVec2(unhide_sz_hit, unhide_sz_hit))
|
|
local unhide_id = window:GetID("#UNHIDE")
|
|
ImGui.KeepAliveID(unhide_id)
|
|
local pressed, hovered, held = ImGui.ButtonBehavior(r, unhide_id, ImGuiButtonFlags.FlattenChildren)
|
|
if pressed then
|
|
node.WantHiddenTabBarToggle = true
|
|
elseif held and ImGui.IsMouseDragging(0) then
|
|
ImGui.StartMouseMovingWindowOrNode(window, node, true) -- Undock from tab-bar triangle = same as window/collapse menu button
|
|
end
|
|
|
|
-- FIXME-DOCK: Ideally we'd use ImGuiCol.TitleBgActive/ImGuiCol.TitleBg here, but neither is guaranteed to be visible enough at this sort of size..
|
|
local col = ImGui.GetColorU32(((held and hovered) or (node.IsFocused and not hovered)) and ImGuiCol.ButtonActive or (hovered and ImGuiCol.ButtonHovered or ImGuiCol.Button))
|
|
window.DrawList:AddTriangleFilled(p, p + ImVec2(unhide_sz_draw, 0.0), p + ImVec2(0.0, unhide_sz_draw), col)
|
|
end
|
|
|
|
-- Scrollbars
|
|
if window.ScrollbarX then
|
|
ImGui.Scrollbar(ImGuiAxis.X)
|
|
end
|
|
if window.ScrollbarY then
|
|
ImGui.Scrollbar(ImGuiAxis.Y)
|
|
end
|
|
|
|
-- Render resize grips (after their input handling so we don't have a frame of latency)
|
|
if handle_borders_and_resize_grips and (bit32.band(flags, ImGuiWindowFlags.NoResize) == 0) then
|
|
for resize_grip_n = 1, #ImGuiResizeGripDef do
|
|
repeat
|
|
local col = resize_grip_col[resize_grip_n]
|
|
if bit32.band(col, IM_COL32_A_MASK) == 0 then
|
|
break
|
|
end
|
|
|
|
local grip = ImGuiResizeGripDef[resize_grip_n]
|
|
local corner = ImLerp(window.Pos, window.Pos + window.Size, grip.CornerPosN)
|
|
local border_inner = IM_ROUND(window_border_size * 0.5)
|
|
window.DrawList:PathLineTo(corner + grip.InnerDir * ((resize_grip_n % 2 == 0) and ImVec2(border_inner, resize_grip_draw_size) or ImVec2(resize_grip_draw_size, border_inner)))
|
|
window.DrawList:PathLineTo(corner + grip.InnerDir * ((resize_grip_n % 2 == 0) and ImVec2(resize_grip_draw_size, border_inner) or ImVec2(border_inner, resize_grip_draw_size)))
|
|
window.DrawList:PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + border_inner), corner.y + grip.InnerDir.y * (window_rounding + border_inner)), window_rounding, grip.AngleMin12, grip.AngleMax12)
|
|
window.DrawList:PathFillConvex(col)
|
|
|
|
until true
|
|
end
|
|
end
|
|
|
|
-- Borders (for dock node host they will be rendered over after the tab bar)
|
|
if handle_borders_and_resize_grips and not window.DockNodeAsHost then
|
|
RenderWindowOuterBorders(window)
|
|
end
|
|
end
|
|
|
|
window.DC.NavLayerCurrent = ImGuiNavLayer.Main
|
|
end
|
|
ImGui.RenderWindowOuterBorders = RenderWindowOuterBorders
|
|
|
|
-- When inside a dock node, this is handled in DockNodeCalcTabBarLayout() instead.
|
|
-- Render title text, collapse button, close button
|
|
-- Returns the updated 'open' value (nil when there is no close button)
|
|
--- @param window ImGuiWindow
|
|
--- @param title_bar_rect ImRect
|
|
--- @param name string
|
|
--- @param open bool?
|
|
--- @return bool?
|
|
local function RenderWindowTitleBarContents(window, title_bar_rect, name, open)
|
|
local g = GImGui
|
|
local style = g.Style
|
|
local flags = window.Flags
|
|
|
|
local has_close_button = (open ~= nil)
|
|
local has_collapse_button = bit32.band(flags, ImGuiWindowFlags.NoCollapse) == 0 and (style.WindowMenuButtonPosition ~= ImGuiDir.None)
|
|
|
|
-- Close & Collapse button are on the Menu NavLayer and don't default focus (unless there's nothing else on that layer)
|
|
-- FIXME-NAV: Might want (or not?) to set the equivalent of ImGuiButtonFlags.NoNavFocus so that mouse clicks on standard title bar items don't necessarily set nav/keyboard ref?
|
|
local item_flags_backup = g.CurrentItemFlags
|
|
g.CurrentItemFlags = bit32.bor(g.CurrentItemFlags, ImGuiItemFlags.NoNavDefaultFocus)
|
|
window.DC.NavLayerCurrent = ImGuiNavLayer.Menu
|
|
|
|
-- Layout buttons
|
|
-- FIXME: Would be nice to generalize the subtleties expressed here into reusable code.
|
|
local pad_l = g.Style.FramePadding.x
|
|
local pad_r = g.Style.FramePadding.x
|
|
local button_sz = g.FontSize
|
|
local close_button_pos
|
|
local collapse_button_pos
|
|
if has_close_button then
|
|
close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y)
|
|
pad_r = pad_r + button_sz + style.ItemInnerSpacing.x
|
|
end
|
|
if has_collapse_button and style.WindowMenuButtonPosition == ImGuiDir.Right then
|
|
collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y)
|
|
pad_r = pad_r + button_sz + style.ItemInnerSpacing.x
|
|
end
|
|
if has_collapse_button and style.WindowMenuButtonPosition == ImGuiDir.Left then
|
|
collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y + style.FramePadding.y)
|
|
pad_l = pad_l + button_sz + style.ItemInnerSpacing.x
|
|
end
|
|
|
|
-- Collapse button (submitting first so it gets priority when choosing a navigation init fallback)
|
|
if has_collapse_button then
|
|
if ImGui.CollapseButton(window:GetID("#COLLAPSE"), collapse_button_pos) then
|
|
window.WantCollapseToggle = true -- Defer actual collapsing to next frame as we are too far in the Begin() function
|
|
end
|
|
end
|
|
|
|
-- Close button
|
|
if has_close_button then
|
|
local backup_item_flags = g.CurrentItemFlags
|
|
g.CurrentItemFlags = bit32.bor(g.CurrentItemFlags, ImGuiItemFlags.NoFocus)
|
|
if ImGui.CloseButton(window:GetID("#CLOSE"), close_button_pos) then
|
|
open = false
|
|
end
|
|
g.CurrentItemFlags = backup_item_flags
|
|
end
|
|
|
|
window.DC.NavLayerCurrent = ImGuiNavLayer.Main
|
|
g.CurrentItemFlags = item_flags_backup
|
|
|
|
-- Title bar text (with: horizontal alignment, avoiding collapse/close button, optional "unsaved document" marker)
|
|
-- FIXME: Refactor text alignment facilities along with RenderText helpers, this is WAY too much messy code..
|
|
local marker_size_x = (bit32.band(flags, ImGuiWindowFlags.UnsavedDocument) ~= 0) and (button_sz * 0.80) or 0.0
|
|
local text_size = ImGui.CalcTextSize(name, nil, true) + ImVec2(marker_size_x, 0.0)
|
|
|
|
-- As a nice touch we try to ensure that centered title text doesn't get affected by visibility of Close/Collapse button,
|
|
-- while uncentered title text will still reach edges correctly.
|
|
if (pad_l > style.FramePadding.x) then
|
|
pad_l = pad_l + g.Style.ItemInnerSpacing.x
|
|
end
|
|
if (pad_r > style.FramePadding.x) then
|
|
pad_r = pad_r + g.Style.ItemInnerSpacing.x
|
|
end
|
|
if (style.WindowTitleAlign.x > 0.0 and style.WindowTitleAlign.x < 1.0) then
|
|
local centerness = ImSaturate(1.0 - ImFabs(style.WindowTitleAlign.x - 0.5) * 2.0) -- 0.0 on either edges, 1.0 on center
|
|
local pad_extend = ImMin(ImMax(pad_l, pad_r), title_bar_rect:GetWidth() - pad_l - pad_r - text_size.x)
|
|
pad_l = ImMax(pad_l, pad_extend * centerness)
|
|
pad_r = ImMax(pad_r, pad_extend * centerness)
|
|
end
|
|
|
|
local layout_r = ImRect(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y, title_bar_rect.Max.x - pad_r, title_bar_rect.Max.y)
|
|
local clip_r = ImRect(layout_r.Min.x, layout_r.Min.y, ImMin(layout_r.Max.x + g.Style.ItemInnerSpacing.x, title_bar_rect.Max.x), layout_r.Max.y)
|
|
|
|
if bit32.band(flags, ImGuiWindowFlags.UnsavedDocument) ~= 0 then
|
|
local marker_pos = ImVec2()
|
|
marker_pos.x = ImClamp(layout_r.Min.x + (layout_r:GetWidth() - text_size.x) * style.WindowTitleAlign.x + text_size.x, layout_r.Min.x, layout_r.Max.x)
|
|
marker_pos.y = (layout_r.Min.y + layout_r.Max.y) * 0.5
|
|
if marker_pos.x > layout_r.Min.x then
|
|
ImGui.RenderBullet(window.DrawList, marker_pos, ImGui.GetColorU32(ImGuiCol.UnsavedMarker))
|
|
clip_r.Max.x = ImMin(clip_r.Max.x, marker_pos.x - math.floor(marker_size_x * 0.5))
|
|
end
|
|
end
|
|
-- if (g.IO.KeyShift) window.DrawList:AddRect(layout_r.Min, layout_r.Max, IM_COL32(255, 128, 0, 255)) -- [DEBUG]
|
|
-- if (g.IO.KeyCtrl) window.DrawList:AddRect(clip_r.Min, clip_r.Max, IM_COL32(255, 128, 0, 255)) -- [DEBUG]
|
|
ImGui.RenderTextClipped(layout_r.Min, layout_r.Max, name, nil, text_size, style.WindowTitleAlign, clip_r)
|
|
|
|
return open
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param flags ImGuiWindowFlags
|
|
--- @param parent_window? ImGuiWindow
|
|
function ImGui.UpdateWindowParentAndRootLinks(window, flags, parent_window)
|
|
window.ParentWindow = parent_window
|
|
window.RootWindow = window
|
|
window.RootWindowPopupTree = window
|
|
window.RootWindowDockTree = window
|
|
window.RootWindowForTitleBarHighlight = window
|
|
window.RootWindowForNav = window
|
|
|
|
if parent_window and (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0) and (bit32.band(flags, ImGuiWindowFlags.Tooltip) == 0) then
|
|
window.RootWindowDockTree = parent_window.RootWindowDockTree
|
|
if not window.DockIsActive and (bit32.band(parent_window.Flags, ImGuiWindowFlags.DockNodeHost) == 0) then
|
|
window.RootWindow = parent_window.RootWindow
|
|
end
|
|
end
|
|
if parent_window and (bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0) then
|
|
window.RootWindowPopupTree = parent_window.RootWindowPopupTree
|
|
end
|
|
if parent_window and (bit32.band(flags, ImGuiWindowFlags.Modal) == 0) and (bit32.band(flags, bit32.bor(ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.Popup, ImGuiWindowFlags.Tooltip)) ~= 0) then -- FIXME: simply use _NoTitleBar ?
|
|
window.RootWindowForTitleBarHighlight = parent_window.RootWindowForTitleBarHighlight
|
|
end
|
|
|
|
while bit32.band(window.RootWindowForNav.ChildFlags, ImGuiChildFlags.NavFlattened) ~= 0 do
|
|
IM_ASSERT(window.RootWindowForNav.ParentWindow ~= nil)
|
|
window.RootWindowForNav = window.RootWindowForNav.ParentWindow
|
|
end
|
|
end
|
|
|
|
-- [EXPERIMENTAL] Called by Begin(). NextWindowData is valid at this point.
|
|
-- This is designed as a toy/test-bed for
|
|
--- @param window ImGuiWindow
|
|
function ImGui.UpdateWindowSkipRefresh(window)
|
|
local g = GImGui
|
|
window.SkipRefresh = false
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy) == 0 then
|
|
return
|
|
end
|
|
|
|
if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.TryToAvoidRefresh) ~= 0 then
|
|
-- [Port] SetNextWindowCached(): refresh on any change since the last refresh (and one frame after being busy)
|
|
if g.NextWindowData.CacheKeySet then
|
|
local function related(w) return w ~= nil and (w.RootWindow == window.RootWindow or ImGui.IsWindowWithinBeginStackOf(w.RootWindow, window)) end
|
|
local busy = window.Appearing or window.Hidden or window.DockIsActive or related(g.HoveredWindow) or related(g.NavWindow) or related(g.ActiveIdWindow) or (g.MovingWindow ~= nil and related(g.MovingWindow))
|
|
local c, key, ds, tex = window.CacheSnap, g.NextWindowData.CacheKeyVal, g.IO.DisplaySize, g.IO.Fonts.TexData
|
|
if busy or c == nil or c.Busy or c.Key ~= key or c.Tex ~= tex or c.Collapsed ~= window.Collapsed
|
|
or c.PosX ~= window.Pos.x or c.PosY ~= window.Pos.y or c.SizeX ~= window.SizeFull.x or c.SizeY ~= window.SizeFull.y
|
|
or c.ScrollX ~= window.Scroll.x or c.ScrollY ~= window.Scroll.y or window.ScrollTarget.x < FLT_MAX or window.ScrollTarget.y < FLT_MAX
|
|
or c.DisplayX ~= ds.x or c.DisplayY ~= ds.y then
|
|
c = c or {}
|
|
window.CacheSnap = c
|
|
c.Busy, c.Key, c.Tex, c.Collapsed = busy, key, tex, window.Collapsed
|
|
c.PosX, c.PosY, c.SizeX, c.SizeY = window.Pos.x, window.Pos.y, window.SizeFull.x, window.SizeFull.y
|
|
c.ScrollX, c.ScrollY, c.DisplayX, c.DisplayY = window.Scroll.x, window.Scroll.y, ds.x, ds.y
|
|
return
|
|
end
|
|
-- skip: widgets early-out on SkipItems and Begin() returns visible=false; End() restores it
|
|
window.CacheSkipItemsBackup = window.SkipItems
|
|
window.SkipItems = true
|
|
end
|
|
-- FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused.
|
|
if window.Appearing then -- If currently appearing
|
|
return
|
|
end
|
|
if window.Hidden then -- If was hidden (previous frame)
|
|
return
|
|
end
|
|
if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.RefreshOnHover) ~= 0 and g.HoveredWindow then
|
|
if window.RootWindow == g.HoveredWindow.RootWindow or ImGui.IsWindowWithinBeginStackOf(g.HoveredWindow.RootWindow, window) then
|
|
return
|
|
end
|
|
end
|
|
if bit32.band(g.NextWindowData.RefreshFlagsVal, ImGuiWindowRefreshFlags.RefreshOnFocus) ~= 0 and g.NavWindow then
|
|
if window.RootWindow == g.NavWindow.RootWindow or ImGui.IsWindowWithinBeginStackOf(g.NavWindow.RootWindow, window) then
|
|
return
|
|
end
|
|
end
|
|
window.DrawList = nil
|
|
window.SkipRefresh = true
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
local function SetWindowActiveForSkipRefresh(window)
|
|
window.Active = true
|
|
for _, child in window.DC.ChildWindows:iter() do
|
|
if not child.Hidden then
|
|
child.Active = true
|
|
child.SkipRefresh = true
|
|
SetWindowActiveForSkipRefresh(child)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow?
|
|
local function SetCurrentWindow(window)
|
|
local g = GImGui
|
|
--- @diagnostic disable-next-line
|
|
g.CurrentWindow = window
|
|
g.CurrentTable = (window and window.DC.CurrentTableIdx ~= -1) and g.Tables:GetByIndex(window.DC.CurrentTableIdx) or nil
|
|
|
|
if window then
|
|
if bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.RendererHasTextures) ~= 0 then
|
|
local viewport = window.Viewport
|
|
g.FontRasterizerDensity = (viewport.FramebufferScale.x ~= 0.0) and viewport.FramebufferScale.x or g.IO.DisplayFramebufferScale.x
|
|
end
|
|
|
|
local backup_skip_items = window.SkipItems
|
|
window.SkipItems = false
|
|
ImGui.UpdateCurrentFontSize(0.0)
|
|
window.SkipItems = backup_skip_items
|
|
ImGui.NavUpdateCurrentWindowIsScrollPushableX()
|
|
end
|
|
end
|
|
|
|
-- TODO:
|
|
function ImGui.GcCompactTransientMiscBuffers()
|
|
local g = GImGui
|
|
g.ItemFlagsStack:clear()
|
|
g.GroupStack:clear()
|
|
g.InputTextLineIndex:clear()
|
|
ImGui.TableGcCompactSettings()
|
|
for _, atlas in g.FontAtlases:iter() do
|
|
atlas:CompactCache()
|
|
end
|
|
end
|
|
|
|
-- Free up/compact internal window buffers, we can use this when a window becomes unused.
|
|
-- Not freed:
|
|
-- - ImGuiWindow, ImGuiWindowSettings, Name, StateStorage, ColumnsStorage (may hold useful data)
|
|
-- This should have no noticeable visual effect. When the window reappear however, expect new allocation/buffer growth/copy cost.
|
|
-- FIXME: Consider exposing of elaborating GC policy, e.g. being able to trim excessive ImDrawList gaps. (#9303)
|
|
--- @param window ImGuiWindow
|
|
function ImGui.GcCompactTransientWindowBuffers(window)
|
|
window.MemoryCompacted = true
|
|
window.MemoryDrawListIdxCapacity = ImMin(math.floor(window.DrawList.IdxBuffer.Size * 1.05), window.DrawList.IdxBuffer.Capacity)
|
|
window.MemoryDrawListVtxCapacity = ImMin(math.floor(window.DrawList.VtxBuffer.Size * 1.05), window.DrawList.VtxBuffer.Capacity)
|
|
window.IDStack:clear()
|
|
window.DrawList:_ClearFreeMemory()
|
|
window.DC.ChildWindows:clear()
|
|
window.DC.ItemWidthStack:clear()
|
|
window.DC.TextWrapPosStack:clear()
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
function ImGui.GcAwakeTransientWindowBuffers(window)
|
|
-- We stored capacity of the ImDrawList buffer to reduce growth-caused allocation/copy when awakening.
|
|
-- The other buffers tends to amortize much faster.
|
|
window.MemoryCompacted = false
|
|
window.DrawList.IdxBuffer:reserve(window.MemoryDrawListIdxCapacity)
|
|
window.DrawList.VtxBuffer:reserve(window.MemoryDrawListVtxCapacity)
|
|
window.MemoryDrawListIdxCapacity = 0
|
|
window.MemoryDrawListVtxCapacity = 0
|
|
end
|
|
|
|
function ImGui.GetWindowWidth()
|
|
local window = GImGui.CurrentWindow
|
|
return window.Size.x
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param pos ImVec2
|
|
--- @param cond? ImGuiCond
|
|
function ImGui.SetWindowPos(window, pos, cond)
|
|
-- Overloads: SetWindowPos(pos, cond) / SetWindowPos(name, pos, cond) / SetWindowPos(window, pos, cond)
|
|
if type(window) == "string" then
|
|
window = ImGui.FindWindowByName(window)
|
|
if window == nil then return end
|
|
elseif window.SetWindowPosAllowFlags == nil then
|
|
window, pos, cond = ImGui.GetCurrentWindowRead(), window, pos
|
|
end
|
|
if cond == nil then cond = 0 end
|
|
|
|
-- Test condition (NB: bit 0 is always true) and clear flags for next time
|
|
if (cond ~= 0) and (bit32.band(window.SetWindowPosAllowFlags, cond) == 0) then
|
|
return
|
|
end
|
|
|
|
IM_ASSERT(cond == 0 or ImIsPowerOfTwo(cond)) -- Make sure the user doesn't attempt to combine multiple condition flags.
|
|
window.SetWindowPosAllowFlags = bit32.band(window.SetWindowPosAllowFlags, bit32.bnot(bit32.bor(ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing)))
|
|
ImVec2_Copy(window.SetWindowPosVal, ImVec2(FLT_MAX, FLT_MAX))
|
|
|
|
-- Set
|
|
local old_pos = ImVec2()
|
|
ImVec2_Copy(old_pos, window.Pos)
|
|
|
|
window.Pos.x = ImTrunc(pos.x)
|
|
window.Pos.y = ImTrunc(pos.y)
|
|
|
|
local offset = window.Pos - old_pos
|
|
|
|
if offset.x == 0 and offset.y == 0 then
|
|
return
|
|
end
|
|
ImGui.MarkIniSettingsDirty(window)
|
|
-- FIXME: share code with TranslateWindow(), need to confirm whether the 3 rect modified by TranslateWindow() are desirable here.
|
|
ImVec2_Copy(window.DC.CursorPos, window.DC.CursorPos + offset) -- As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor
|
|
ImVec2_Copy(window.DC.CursorMaxPos, window.DC.CursorMaxPos + offset) -- And more importantly we need to offset CursorMaxPos/CursorStartPos this so ContentSize calculation doesn't get affected.
|
|
ImVec2_Copy(window.DC.IdealMaxPos, window.DC.IdealMaxPos + offset)
|
|
ImVec2_Copy(window.DC.CursorStartPos, window.DC.CursorStartPos + offset)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param size ImVec2
|
|
--- @param cond? ImGuiCond
|
|
function ImGui.SetWindowSize(window, size, cond)
|
|
-- Overloads: SetWindowSize(size, cond) / SetWindowSize(name, size, cond) / SetWindowSize(window, size, cond)
|
|
if type(window) == "string" then
|
|
window = ImGui.FindWindowByName(window)
|
|
if window == nil then return end
|
|
elseif window.SetWindowSizeAllowFlags == nil then
|
|
window, size, cond = GImGui.CurrentWindow, window, size
|
|
end
|
|
if cond == nil then cond = 0 end
|
|
|
|
-- Test condition (NB: bit 0 is always true) and clear flags for next time
|
|
if ((cond ~= 0) and bit32.band(window.SetWindowSizeAllowFlags, cond) == 0) then
|
|
return
|
|
end
|
|
|
|
IM_ASSERT(cond == 0 or ImIsPowerOfTwo(cond)) -- Make sure the user doesn't attempt to combine multiple condition flags.
|
|
window.SetWindowSizeAllowFlags = bit32.band(window.SetWindowSizeAllowFlags, bit32.bnot(bit32.bor(ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing)))
|
|
|
|
-- Enable auto-fit (not done in BeginChild() path unless appearing or combined with ImGuiChildFlags.AlwaysAutoResize)
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0 or window.Appearing or bit32.band(window.ChildFlags, ImGuiChildFlags.AlwaysAutoResize) ~= 0 then
|
|
window.AutoFitFramesX = (size.x <= 0.0) and 2 or 0
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0 or window.Appearing or bit32.band(window.ChildFlags, ImGuiChildFlags.AlwaysAutoResize) ~= 0 then
|
|
window.AutoFitFramesY = (size.y <= 0.0) and 2 or 0
|
|
end
|
|
|
|
-- Set
|
|
local old_size_x, old_size_y = window.SizeFull.x, window.SizeFull.y
|
|
|
|
if size.x <= 0.0 then
|
|
window.AutoFitOnlyGrows = false
|
|
else
|
|
window.SizeFull.x = IM_TRUNC(size.x)
|
|
end
|
|
if size.y <= 0.0 then
|
|
window.AutoFitOnlyGrows = false
|
|
else
|
|
window.SizeFull.y = IM_TRUNC(size.y)
|
|
end
|
|
if old_size_x ~= window.SizeFull.x or old_size_y ~= window.SizeFull.y then
|
|
ImGui.MarkIniSettingsDirty(window)
|
|
end
|
|
end
|
|
|
|
--- Overloads: SetWindowCollapsed(collapsed, cond) / SetWindowCollapsed(name, collapsed, cond) / SetWindowCollapsed(window, collapsed, cond)
|
|
function ImGui.SetWindowCollapsed(window, collapsed, cond)
|
|
if type(window) == "string" then
|
|
window = ImGui.FindWindowByName(window)
|
|
if window == nil then return end
|
|
elseif type(window) == "boolean" then
|
|
window, collapsed, cond = GImGui.CurrentWindow, window, collapsed
|
|
end
|
|
if cond == nil then cond = 0 end
|
|
-- Test condition (NB: bit 0 is always true) and clear flags for next time
|
|
if cond ~= 0 and bit32.band(window.SetWindowCollapsedAllowFlags, cond) == 0 then
|
|
return
|
|
end
|
|
window.SetWindowCollapsedAllowFlags = bit32.band(window.SetWindowCollapsedAllowFlags, bit32.bnot(bit32.bor(ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing)))
|
|
|
|
-- Queue applying in Begin()
|
|
if window.WantCollapseToggle then
|
|
window.Collapsed = not window.Collapsed
|
|
end
|
|
window.WantCollapseToggle = (window.Collapsed ~= collapsed)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param pos ImVec2
|
|
--- @param size ImVec2
|
|
function ImGui.SetWindowHitTestHole(window, pos, size)
|
|
IM_ASSERT(window.HitTestHoleSize.x == 0) -- We don't support multiple holes/hit test filters
|
|
window.HitTestHoleSize = ImVec2(math.floor(size.x), math.floor(size.y))
|
|
window.HitTestHoleOffset = ImVec2(math.floor(pos.x - window.Pos.x), math.floor(pos.y - window.Pos.y))
|
|
end
|
|
|
|
function ImGui.IsWindowCollapsed()
|
|
return ImGui.GetCurrentWindowRead().Collapsed
|
|
end
|
|
|
|
function ImGui.IsWindowAppearing()
|
|
return ImGui.GetCurrentWindowRead().Appearing
|
|
end
|
|
|
|
function ImGui.GetWindowHeight()
|
|
return GImGui.CurrentWindow.Size.y
|
|
end
|
|
|
|
--- @return ImVec2 # Ref, do not modify
|
|
function ImGui.GetWindowPos()
|
|
return GImGui.CurrentWindow.Pos
|
|
end
|
|
|
|
--- @return ImVec2 # Ref, do not modify
|
|
function ImGui.GetWindowSize()
|
|
return ImGui.GetCurrentWindowRead().Size
|
|
end
|
|
|
|
-- Overloads: SetWindowFocus() / SetWindowFocus(name). Passing nil name removes focus.
|
|
--- @param name? string
|
|
function ImGui.SetWindowFocus(...)
|
|
if select("#", ...) == 0 then
|
|
ImGui.FocusWindow(GImGui.CurrentWindow)
|
|
return
|
|
end
|
|
local name = ...
|
|
if name then
|
|
local window = ImGui.FindWindowByName(name)
|
|
if window then
|
|
ImGui.FocusWindow(window)
|
|
end
|
|
else
|
|
ImGui.FocusWindow(nil)
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
function ImGui.SetWindowHiddenAndSkipItemsForCurrentFrame(window)
|
|
window.Hidden = true
|
|
window.SkipItems = true
|
|
window.HiddenFramesCanSkipItems = 1
|
|
end
|
|
|
|
--- @param pos ImVec2
|
|
--- @param cond? ImGuiCond
|
|
--- @param pivot? ImVec2
|
|
function ImGui.SetNextWindowPos(pos, cond, pivot)
|
|
if cond == nil then cond = 0 end
|
|
if pivot == nil then pivot = ImVec2(0, 0) end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(cond == 0 or ImIsPowerOfTwo(cond)) -- Make sure the user doesn't attempt to combine multiple condition flags.
|
|
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasPos)
|
|
ImVec2_Copy(g.NextWindowData.PosVal, pos)
|
|
ImVec2_Copy(g.NextWindowData.PosPivotVal, pivot)
|
|
g.NextWindowData.PosCond = (cond ~= 0) and cond or ImGuiCond.Always
|
|
g.NextWindowData.PosUndock = true
|
|
end
|
|
|
|
--- @param size ImVec2
|
|
--- @param cond? ImGuiCond
|
|
function ImGui.SetNextWindowSize(size, cond)
|
|
if cond == nil then cond = 0 end
|
|
|
|
local g = GImGui
|
|
IM_ASSERT(cond == 0 or ImIsPowerOfTwo(cond)) -- Make sure the user doesn't attempt to combine multiple condition flags.
|
|
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSize)
|
|
ImVec2_Copy(g.NextWindowData.SizeVal, size)
|
|
g.NextWindowData.SizeCond = (cond ~= 0) and cond or ImGuiCond.Always
|
|
end
|
|
|
|
-- For each axis:
|
|
-- - Use 0.0f as min or FLT_MAX as max if you don't want limits, e.g. size_min = (500.0f, 0.0f), size_max = (FLT_MAX, FLT_MAX) sets a minimum width.
|
|
-- - Use -1 for both min and max of same axis to preserve current size which itself is a constraint.
|
|
-- - See "Demo->Examples->Constrained-resizing window" for examples.
|
|
--- @param size_min ImVec2
|
|
--- @param size_max ImVec2
|
|
--- @param custom_callback? function
|
|
--- @param custom_callback_user_data? any
|
|
function ImGui.SetNextWindowSizeConstraints(size_min, size_max, custom_callback, custom_callback_user_data)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSizeConstraint)
|
|
g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max)
|
|
g.NextWindowData.SizeCallback = custom_callback
|
|
g.NextWindowData.SizeCallbackUserData = custom_callback_user_data
|
|
end
|
|
|
|
--- @param alpha float
|
|
function ImGui.SetNextWindowBgAlpha(alpha)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasBgAlpha)
|
|
g.NextWindowData.BgAlphaVal = alpha
|
|
end
|
|
|
|
function ImGui.GetWindowDrawList()
|
|
local window = ImGui.GetCurrentWindow()
|
|
return window.DrawList
|
|
end
|
|
|
|
function ImGui.GetFont()
|
|
return GImGui.Font
|
|
end
|
|
|
|
-- Get current font size (= height in pixels) of current font, with global scale factors applied.
|
|
-- - Use style.FontSizeBase to get value before global scale factors.
|
|
-- - recap: ImGui.GetFontSize() == style.FontSizeBase * (style.FontScaleMain * style.FontScaleDpi * other_scaling_factors)
|
|
function ImGui.GetFontSize()
|
|
return GImGui.FontSize
|
|
end
|
|
|
|
--- @return ImVec2
|
|
function ImGui.GetFontTexUvWhitePixel()
|
|
return GImGui.DrawListSharedData.TexUvWhitePixel
|
|
end
|
|
|
|
-- This is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations.
|
|
-- FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version if needed?
|
|
function ImGui.SetActiveIdUsingAllKeyboardKeys()
|
|
local g = GImGui
|
|
IM_ASSERT(g.ActiveId ~= 0)
|
|
g.ActiveIdUsingNavDirMask = (bit32.lshift(1, ImGuiDir.COUNT) - 1)
|
|
g.ActiveIdUsingAllKeyboardKeys = true
|
|
ImGui.NavMoveRequestCancel()
|
|
end
|
|
|
|
--- @return ImVec2 # Ref
|
|
function ImGui.GetItemRectMin()
|
|
local g = GImGui
|
|
return g.LastItemData.Rect.Min
|
|
end
|
|
|
|
--- @return ImVec2 # Ref
|
|
function ImGui.GetItemRectMax()
|
|
local g = GImGui
|
|
return g.LastItemData.Rect.Max
|
|
end
|
|
|
|
--- @param name string
|
|
--- @param id ImGuiID
|
|
--- @param size_arg ImVec2
|
|
--- @param child_flags ImGuiChildFlags
|
|
--- @param window_flags ImGuiWindowFlags
|
|
function ImGui.BeginChildEx(name, id, size_arg, child_flags, window_flags)
|
|
local g = GImGui
|
|
local parent_window = g.CurrentWindow
|
|
IM_ASSERT(id ~= 0)
|
|
if child_flags == nil then child_flags = 0 end
|
|
if window_flags == nil then window_flags = 0 end
|
|
if size_arg == nil then size_arg = ImVec2(0, 0) end
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasChildFlags) ~= 0 then
|
|
child_flags = bit32.band(bit32.bor(child_flags, g.NextWindowData.ChildFlagsSet), bit32.bnot(g.NextWindowData.ChildFlagsClear))
|
|
end
|
|
|
|
-- Sanity check as it is likely that some user will accidentally pass ImGuiWindowFlags into the ImGuiChildFlags argument.
|
|
local ImGuiChildFlags_SupportedMask_ = bit32.bor(ImGuiChildFlags.Borders, ImGuiChildFlags.AlwaysUseWindowPadding, ImGuiChildFlags.ResizeX, ImGuiChildFlags.ResizeY, ImGuiChildFlags.AutoResizeX, ImGuiChildFlags.AutoResizeY, ImGuiChildFlags.AlwaysAutoResize, ImGuiChildFlags.FrameStyle, ImGuiChildFlags.NavFlattened)
|
|
-- IM_UNUSED(ImGuiChildFlags_SupportedMask_)
|
|
IM_ASSERT(bit32.band(child_flags, bit32.bnot(ImGuiChildFlags_SupportedMask_)) == 0, "Illegal ImGuiChildFlags value. Did you pass ImGuiWindowFlags values instead of ImGuiChildFlags?")
|
|
IM_ASSERT(bit32.band(window_flags, ImGuiWindowFlags.AlwaysAutoResize) == 0, "Cannot specify ImGuiWindowFlags.AlwaysAutoResize for BeginChild(). Use ImGuiChildFlags.AlwaysAutoResize!")
|
|
if bit32.band(child_flags, ImGuiChildFlags.AlwaysAutoResize) ~= 0 then
|
|
IM_ASSERT(bit32.band(child_flags, bit32.bor(ImGuiChildFlags.ResizeX, ImGuiChildFlags.ResizeY)) == 0, "Cannot use ImGuiChildFlags.ResizeX or ImGuiChildFlags.ResizeY with ImGuiChildFlags.AlwaysAutoResize!")
|
|
IM_ASSERT(bit32.band(child_flags, bit32.bor(ImGuiChildFlags.AutoResizeX, ImGuiChildFlags.AutoResizeY)) ~= 0, "Must use ImGuiChildFlags.AutoResizeX or ImGuiChildFlags.AutoResizeY with ImGuiChildFlags.AlwaysAutoResize!")
|
|
end
|
|
if bit32.band(child_flags, ImGuiChildFlags.AutoResizeX) ~= 0 then
|
|
child_flags = bit32.band(child_flags, bit32.bnot(ImGuiChildFlags.ResizeX))
|
|
end
|
|
if bit32.band(child_flags, ImGuiChildFlags.AutoResizeY) ~= 0 then
|
|
child_flags = bit32.band(child_flags, bit32.bnot(ImGuiChildFlags.ResizeY))
|
|
end
|
|
|
|
-- Set window flags
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoDocking)
|
|
window_flags = bit32.bor(window_flags, bit32.band(parent_window.Flags, ImGuiWindowFlags.NoMove)) -- Inherit the NoMove flag
|
|
if bit32.band(child_flags, bit32.bor(ImGuiChildFlags.AutoResizeX, ImGuiChildFlags.AutoResizeY, ImGuiChildFlags.AlwaysAutoResize)) ~= 0 then
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.AlwaysAutoResize) -- FIXME: Would be sane to not make single-axis flag set this. (#9355)
|
|
end
|
|
if bit32.band(child_flags, bit32.bor(ImGuiChildFlags.ResizeX, ImGuiChildFlags.ResizeY)) == 0 then
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoSavedSettings)
|
|
end
|
|
|
|
-- Special framed style
|
|
if bit32.band(child_flags, ImGuiChildFlags.FrameStyle) ~= 0 then
|
|
ImGui.PushStyleColor(ImGuiCol.ChildBg, g.Style.Colors[ImGuiCol.FrameBg])
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, g.Style.FrameRounding)
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ChildBorderSize, g.Style.FrameBorderSize)
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, g.Style.FramePadding)
|
|
child_flags = bit32.bor(child_flags, ImGuiChildFlags.Borders, ImGuiChildFlags.AlwaysUseWindowPadding)
|
|
window_flags = bit32.bor(window_flags, ImGuiWindowFlags.NoMove)
|
|
end
|
|
|
|
-- Forward size
|
|
-- Important: Begin() has special processing to switch condition to ImGuiCond.FirstUseEver for a given axis when ImGuiChildFlags.ResizeXXX is set.
|
|
-- (the alternative would to store conditional flags per axis, which is possible but more code)
|
|
local size_avail = ImGui.GetContentRegionAvail()
|
|
local size_default = ImVec2((bit32.band(child_flags, ImGuiChildFlags.AutoResizeX) ~= 0) and 0.0 or size_avail.x, (bit32.band(child_flags, ImGuiChildFlags.AutoResizeY) ~= 0) and 0.0 or size_avail.y)
|
|
local size = ImGui.CalcItemSize(size_arg, size_default.x, size_default.y)
|
|
|
|
-- A SetNextWindowSize() call always has priority (#8020)
|
|
-- (since the code in Begin() never supported SizeVal==0.0 aka auto-resize via SetNextWindowSize() call, we don't support it here for now)
|
|
-- FIXME: We only support ImGuiCond.Always in this path. Supporting other paths would requires to obtain window pointer.
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSize) ~= 0 and bit32.band(g.NextWindowData.SizeCond, ImGuiCond.Always) ~= 0 then
|
|
if g.NextWindowData.SizeVal.x > 0.0 then
|
|
size.x = g.NextWindowData.SizeVal.x
|
|
child_flags = bit32.band(child_flags, bit32.bnot(ImGuiChildFlags.ResizeX))
|
|
end
|
|
if g.NextWindowData.SizeVal.y > 0.0 then
|
|
size.y = g.NextWindowData.SizeVal.y
|
|
child_flags = bit32.band(child_flags, bit32.bnot(ImGuiChildFlags.ResizeY))
|
|
end
|
|
end
|
|
ImGui.SetNextWindowSize(size)
|
|
ImGui.SetNextWindowChildFlags(child_flags, true)
|
|
|
|
-- Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value.
|
|
-- FIXME: 2023/11/14: commented out shorted version. We had an issue with multiple ### in child window path names, which the trailing hash helped workaround.
|
|
-- e.g. "ParentName###ParentIdentifier/ChildName###ChildIdentifier" would get hashed incorrectly by ImHashStr(), trailing _%08X somehow fixes it.
|
|
local temp_window_name
|
|
-- if (name and parent_window.IDStack:back() == parent_window.ID)
|
|
-- temp_window_name = ImFormatString("%s/%s", parent_window.Name, name) -- May omit ID if in root of ID stack
|
|
-- else
|
|
if name then
|
|
temp_window_name = ImFormatString("%s/%s_%08X", parent_window.Name, name, id)
|
|
else
|
|
temp_window_name = ImFormatString("%s/%08X", parent_window.Name, id)
|
|
end
|
|
|
|
-- Set style
|
|
local backup_border_size = g.Style.ChildBorderSize
|
|
if bit32.band(child_flags, ImGuiChildFlags.Borders) == 0 then
|
|
g.Style.ChildBorderSize = 0.0
|
|
end
|
|
|
|
-- Begin into window
|
|
local _, ret = ImGui.Begin(temp_window_name, nil, window_flags)
|
|
|
|
-- Restore style
|
|
g.Style.ChildBorderSize = backup_border_size
|
|
|
|
if bit32.band(child_flags, ImGuiChildFlags.FrameStyle) ~= 0 then
|
|
ImGui.PopStyleVar(3)
|
|
ImGui.PopStyleColor()
|
|
end
|
|
|
|
local child_window = g.CurrentWindow
|
|
child_window.ChildId = id
|
|
|
|
-- Set the cursor to handle case where the user called SetNextWindowPos()+BeginChild() manually.
|
|
-- While this is not really documented/defined, it seems that the expected thing to do.
|
|
if child_window.BeginCount == 1 then
|
|
ImVec2_Copy(parent_window.DC.CursorPos, child_window.Pos)
|
|
end
|
|
|
|
-- Process navigation-in immediately so NavInit can run on first frame
|
|
-- Can enter a child if (A) it has navigable items or (B) it can be scrolled.
|
|
local temp_id_for_activation = ImHashStr("##Child", nil, id)
|
|
if g.ActiveId == temp_id_for_activation then
|
|
ImGui.ClearActiveID()
|
|
end
|
|
if g.NavActivateId == id and bit32.band(child_flags, ImGuiChildFlags.NavFlattened) == 0 and (child_window.DC.NavLayersActiveMask ~= 0 or child_window.DC.NavWindowHasScrollY) then
|
|
ImGui.FocusWindow(child_window)
|
|
ImGui.NavInitWindow(child_window, false)
|
|
ImGui.SetActiveID(temp_id_for_activation, child_window) -- Steal ActiveId with another arbitrary id so that key-press won't activate child item
|
|
g.ActiveIdSource = g.NavInputSource
|
|
end
|
|
|
|
return ret
|
|
end
|
|
|
|
-- Prior to v1.90 2023/10/16, the BeginChild() function took a 'bool border = false' parameter instead of 'ImGuiChildFlags child_flags = 0'.
|
|
-- ImGuiChildFlags.Borders is defined as always == 1 in order to allow old code passing 'true'. Read comments in imgui.h for details!
|
|
-- `true` is accepted as ImGuiChildFlags.Borders for legacy code.
|
|
--- @param str_id_or_id string|ImGuiID
|
|
--- @param size_arg? ImVec2
|
|
--- @param child_flags? ImGuiChildFlags|bool
|
|
--- @param window_flags? ImGuiWindowFlags
|
|
--- @return bool # visible (always call EndChild()!)
|
|
function ImGui.BeginChild(str_id_or_id, size_arg, child_flags, window_flags)
|
|
if child_flags == true then child_flags = ImGuiChildFlags.Borders elseif not child_flags then child_flags = 0 end
|
|
if type(str_id_or_id) == "number" then
|
|
return ImGui.BeginChildEx(nil, str_id_or_id, size_arg, child_flags, window_flags)
|
|
end
|
|
local id = ImGui.GetCurrentWindow():GetID(str_id_or_id)
|
|
return ImGui.BeginChildEx(str_id_or_id, id, size_arg, child_flags, window_flags)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param rect ImRect
|
|
function ImGui.SetLastItemDataForChildWindowItem(window, rect)
|
|
local g = GImGui
|
|
ImGui.SetLastItemData(window.ChildId, g.CurrentItemFlags, window.DC.ChildItemStatusFlags or 0, rect)
|
|
end
|
|
|
|
function ImGui.EndChild()
|
|
local g = GImGui
|
|
local child_window = g.CurrentWindow
|
|
|
|
local backup_within_end_child_id = g.WithinEndChildID
|
|
IM_ASSERT(bit32.band(child_window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0) -- Mismatched BeginChild()/EndChild() calls
|
|
|
|
g.WithinEndChildID = child_window.ID
|
|
local child_size = ImVec2(child_window.Size.x, child_window.Size.y)
|
|
ImGui.End()
|
|
if child_window.BeginCount == 1 then
|
|
local parent_window = g.CurrentWindow
|
|
local bb = ImRect(parent_window.DC.CursorPos, parent_window.DC.CursorPos + child_size)
|
|
ImGui.ItemSize(child_size)
|
|
local nav_flattened = bit32.band(child_window.ChildFlags, ImGuiChildFlags.NavFlattened) ~= 0
|
|
if (child_window.DC.NavLayersActiveMask ~= 0 or child_window.DC.NavWindowHasScrollY) and not nav_flattened then
|
|
ImGui.ItemAdd(bb, child_window.ChildId)
|
|
ImGui.RenderNavCursor(bb, child_window.ChildId)
|
|
|
|
-- When browsing a window that has no activable items (scroll only) we keep a highlight on the child (pass g.NavId to trick into always displaying)
|
|
if child_window.DC.NavLayersActiveMask == 0 and child_window == g.NavWindow then
|
|
ImGui.RenderNavCursor(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavRenderCursorFlags.Compact)
|
|
end
|
|
else
|
|
-- Not navigable into
|
|
-- - This is a bit of a fringe use case, mostly useful for undecorated, non-scrolling contents childs, or empty childs.
|
|
-- - We could later decide to not apply this path if ImGuiChildFlags.FrameStyle or ImGuiChildFlags.Borders is set.
|
|
ImGui.ItemAdd(bb, child_window.ChildId, nil, ImGuiItemFlags.NoNav)
|
|
|
|
-- But when flattened we directly reach items, adjust active layer mask accordingly
|
|
if nav_flattened then
|
|
parent_window.DC.NavLayersActiveMaskNext = bit32.bor(parent_window.DC.NavLayersActiveMaskNext, child_window.DC.NavLayersActiveMaskNext)
|
|
end
|
|
end
|
|
if g.HoveredWindow == child_window then
|
|
g.LastItemData.StatusFlags = bit32.bor(g.LastItemData.StatusFlags, ImGuiItemStatusFlags.HoveredWindow)
|
|
end
|
|
child_window.DC.ChildItemStatusFlags = g.LastItemData.StatusFlags
|
|
-- SetLastItemDataForChildWindowItem(child_window, child_window:Rect()) -- Not needed, effectively done by ItemAdd()
|
|
else
|
|
ImGui.SetLastItemDataForChildWindowItem(child_window, child_window:Rect())
|
|
end
|
|
|
|
g.WithinEndChildID = backup_within_end_child_id
|
|
g.LogLinePosY = -FLT_MAX -- To enforce a carriage return
|
|
end
|
|
|
|
--- @param flags ImGuiChildFlags
|
|
--- @param enabled bool
|
|
function ImGui.SetNextWindowChildFlags(flags, enabled)
|
|
local g = GImGui
|
|
local nwd = g.NextWindowData
|
|
if bit32.band(nwd.HasFlags, ImGuiNextWindowDataFlags.HasChildFlags) == 0 then
|
|
nwd.ChildFlagsSet = 0; nwd.ChildFlagsClear = 0
|
|
end
|
|
if enabled then
|
|
nwd.ChildFlagsSet = bit32.bor(nwd.ChildFlagsSet, flags)
|
|
nwd.ChildFlagsClear = bit32.band(nwd.ChildFlagsClear, bit32.bnot(flags))
|
|
else
|
|
nwd.ChildFlagsSet = bit32.band(nwd.ChildFlagsSet, bit32.bnot(flags))
|
|
nwd.ChildFlagsClear = bit32.bor(nwd.ChildFlagsClear, flags)
|
|
end
|
|
nwd.HasFlags = bit32.bor(nwd.HasFlags, ImGuiNextWindowDataFlags.HasChildFlags)
|
|
end
|
|
|
|
--- @param flags ImGuiWindowFlags
|
|
--- @param enabled bool
|
|
function ImGui.SetNextWindowFlags(flags, enabled)
|
|
local g = GImGui
|
|
local nwd = g.NextWindowData
|
|
if bit32.band(nwd.HasFlags, ImGuiNextWindowDataFlags.HasWindowFlags) == 0 then
|
|
nwd.WindowFlagsSet = 0; nwd.WindowFlagsClear = 0
|
|
end
|
|
if enabled then
|
|
nwd.WindowFlagsSet = bit32.bor(nwd.WindowFlagsSet, flags)
|
|
nwd.WindowFlagsClear = bit32.band(nwd.WindowFlagsClear, bit32.bnot(flags))
|
|
else
|
|
nwd.WindowFlagsSet = bit32.band(nwd.WindowFlagsSet, bit32.bnot(flags))
|
|
nwd.WindowFlagsClear = bit32.bor(nwd.WindowFlagsClear, flags)
|
|
end
|
|
nwd.HasFlags = bit32.bor(nwd.HasFlags, ImGuiNextWindowDataFlags.HasWindowFlags)
|
|
end
|
|
|
|
-- Content size = inner scrollable rectangle, padded with WindowPadding.
|
|
-- SetNextWindowContentSize(ImVec2(100,100)) + ImGuiWindowFlags.AlwaysAutoResize will always allow submitting a 100x100 item.
|
|
--- @param size ImVec2
|
|
function ImGui.SetNextWindowContentSize(size)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasContentSize)
|
|
ImVec2_CopyV(g.NextWindowData.ContentSizeVal, ImTrunc(size.x), ImTrunc(size.y))
|
|
end
|
|
|
|
--- @param scroll ImVec2
|
|
function ImGui.SetNextWindowScroll(scroll)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasScroll)
|
|
ImVec2_Copy(g.NextWindowData.ScrollVal, scroll)
|
|
end
|
|
|
|
--- @param collapsed bool
|
|
--- @param cond? ImGuiCond
|
|
function ImGui.SetNextWindowCollapsed(collapsed, cond)
|
|
if cond == nil then cond = 0 end
|
|
local g = GImGui
|
|
IM_ASSERT(cond == 0 or ImIsPowerOfTwo(cond)) -- Make sure the user doesn't attempt to combine multiple condition flags.
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasCollapsed)
|
|
g.NextWindowData.CollapsedVal = collapsed
|
|
g.NextWindowData.CollapsedCond = (cond ~= 0) and cond or ImGuiCond.Always
|
|
end
|
|
|
|
function ImGui.SetNextWindowFocus()
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasFocus)
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
function ImGui.SetNextWindowViewport(id)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasViewport)
|
|
g.NextWindowData.ViewportId = id
|
|
end
|
|
|
|
--- @param window_class ImGuiWindowClass
|
|
function ImGui.SetNextWindowClass(window_class)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasWindowClass)
|
|
local wc = ImGuiWindowClass()
|
|
for k, v in pairs(window_class) do wc[k] = v end -- C++ copies the struct
|
|
g.NextWindowData.WindowClass = wc
|
|
end
|
|
|
|
-- This is experimental and meant to be a toy for exploring a future/wider range of features.
|
|
--- @param flags ImGuiWindowRefreshFlags
|
|
function ImGui.SetNextWindowRefreshPolicy(flags)
|
|
local g = GImGui
|
|
g.NextWindowData.HasFlags = bit32.bor(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasRefreshPolicy)
|
|
g.NextWindowData.RefreshFlagsVal = flags
|
|
g.NextWindowData.CacheKeySet = false
|
|
end
|
|
|
|
-- [Port] Opt-in cached window (built on the refresh policy above). While nothing that could change the window
|
|
-- happened, Begin() keeps last frame's draw list and returns visible=false, so `if visible then ... end` skips your code.
|
|
-- It refreshes when `key` changes (pass e.g. a version number of the data shown), while the window (or a popup/child
|
|
-- of it) is hovered, focused or active and for one frame after, and when it appears, moves, resizes, scrolls,
|
|
-- collapses, is docked, or the display size or font atlas texture changes.
|
|
-- Anything else that changes the contents (style, fonts, time) must be part of `key`.
|
|
function ImGui.SetNextWindowCached(key)
|
|
local g = GImGui
|
|
ImGui.SetNextWindowRefreshPolicy(bit32.bor(ImGuiWindowRefreshFlags.TryToAvoidRefresh, ImGuiWindowRefreshFlags.RefreshOnHover, ImGuiWindowRefreshFlags.RefreshOnFocus))
|
|
g.NextWindowData.CacheKeySet = true
|
|
g.NextWindowData.CacheKeyVal = key
|
|
end
|
|
|
|
function ImGui.GetWindowDpiScale()
|
|
return 1.0 -- Roblox: single viewport, no DPI scaling
|
|
end
|
|
|
|
function ImGui.GetWindowViewport()
|
|
local g = GImGui
|
|
return g.CurrentViewport or ImGui.GetMainViewport()
|
|
end
|
|
|
|
-- Prefer using PushFont(nil, style.FontSizeBase * factor), or use style.FontScaleMain to scale all windows.
|
|
--- @param scale float
|
|
function ImGui.SetWindowFontScale(scale)
|
|
IM_ASSERT(scale > 0.0)
|
|
local window = ImGui.GetCurrentWindow()
|
|
window.FontWindowScale = scale
|
|
ImGui.UpdateCurrentFontSize(0.0)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
function ImGui.StartMouseMovingWindow(window)
|
|
-- Set ActiveId even if the _NoMove flag is set. Without it, dragging away from a window with _NoMove would activate hover on other windows.
|
|
-- We _also_ call this when clicking in a window empty space when io.ConfigWindowsMoveFromTitleBarOnly is set, but clear g.MovingWindow afterward.
|
|
-- This is because we want ActiveId to be set even when the window is not permitted to move.
|
|
local g = GImGui
|
|
ImGui.FocusWindow(window)
|
|
ImGui.SetActiveID(window.MoveId, window)
|
|
if (g.IO.ConfigNavCursorVisibleAuto) then
|
|
g.NavCursorVisible = false
|
|
end
|
|
g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window.RootWindowDockTree.Pos
|
|
g.ActiveIdNoClearOnFocusLoss = true
|
|
ImGui.SetActiveIdUsingAllKeyboardKeys()
|
|
|
|
local can_move_window = true
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoMove) ~= 0 or bit32.band(window.RootWindowDockTree.Flags, ImGuiWindowFlags.NoMove) ~= 0 then
|
|
can_move_window = false
|
|
end
|
|
local node = window.DockNodeAsHost
|
|
if node and node.VisibleWindow and bit32.band(node.VisibleWindow.Flags, ImGuiWindowFlags.NoMove) ~= 0 then
|
|
can_move_window = false
|
|
end
|
|
if can_move_window then
|
|
g.MovingWindow = window
|
|
end
|
|
end
|
|
|
|
-- Handle mouse moving window
|
|
-- Note: moving window with the navigation keys (Square + d-pad / Ctrl+Tab + Arrows) are processed in NavUpdateWindowing()
|
|
-- FIXME: We don't have strong guarantee that g.MovingWindow stay synced with g.ActiveId == g.MovingWindow.MoveId.
|
|
-- This is currently enforced by the fact that BeginDragDropSource() is setting all g.ActiveIdUsingXXXX flags to inhibit navigation inputs,
|
|
-- but if we should more thoroughly test cases where g.ActiveId or g.MovingWindow gets changed and not the other.
|
|
function ImGui.UpdateMouseMovingWindowNewFrame()
|
|
local g = GImGui
|
|
|
|
if g.MovingWindow then
|
|
-- We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window).
|
|
-- We track it to preserve Focus and so that generally ActiveIdWindow == MovingWindow and ActiveId == MovingWindow.MoveId for consistency.
|
|
ImGui.KeepAliveID(g.ActiveId)
|
|
IM_ASSERT(g.MovingWindow and g.MovingWindow.RootWindowDockTree)
|
|
|
|
local moving_window = g.MovingWindow.RootWindowDockTree
|
|
|
|
-- When a window stop being submitted while being dragged, it may will its viewport until next Begin()
|
|
local window_disappeared = (not moving_window.WasActive and not moving_window.Active)
|
|
if g.IO.MouseDown[0] and ImGui.IsMousePosValid(g.IO.MousePos) and not window_disappeared then
|
|
local pos = g.IO.MousePos - g.ActiveIdClickOffset
|
|
if moving_window.Pos.x ~= pos.x or moving_window.Pos.y ~= pos.y then
|
|
ImGui.SetWindowPos(moving_window, pos, ImGuiCond.Always)
|
|
if moving_window.Viewport and moving_window.ViewportOwned then -- Synchronize viewport immediately because some overlays may relies on clipping rectangle before we Begin() into the window.
|
|
ImVec2_Copy(moving_window.Viewport.Pos, pos)
|
|
moving_window.Viewport:UpdateWorkRect()
|
|
end
|
|
end
|
|
ImGui.FocusWindow(g.MovingWindow)
|
|
else
|
|
ImGui.StopMouseMovingWindow()
|
|
ImGui.ClearActiveID()
|
|
end
|
|
else
|
|
-- When clicking/dragging from a window that has the _NoMove flag, we still set the ActiveId in order to prevent hovering others.
|
|
if (g.ActiveIdWindow and g.ActiveIdWindow.MoveId == g.ActiveId) then
|
|
ImGui.KeepAliveID(g.ActiveId)
|
|
|
|
if not g.IO.MouseDown[0] then
|
|
ImGui.ClearActiveID()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.GetDrawListSharedData()
|
|
return GImGui.DrawListSharedData
|
|
end
|
|
|
|
-- Supported flags: ImGuiPopupFlags.AnyPopupId, ImGuiPopupFlags.AnyPopupLevel
|
|
--- @param id ImGuiID
|
|
--- @param popup_flags ImGuiPopupFlags
|
|
function ImGui.IsPopupOpen(id, popup_flags)
|
|
local g = GImGui
|
|
if popup_flags == nil then popup_flags = 0 end
|
|
if type(id) == "string" then
|
|
-- str_id overload: with AnyPopupId the id must be 0, otherwise it is hashed with the current window's ID stack
|
|
id = (bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupId) ~= 0) and 0 or g.CurrentWindow:GetID(id)
|
|
end
|
|
|
|
if bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupId) ~= 0 then
|
|
-- Return true if any popup is open at the current BeginPopup() level of the popup stack
|
|
-- This may be used to e.g. test for another popups already opened to handle popups priorities at the same level.
|
|
IM_ASSERT(id == 0)
|
|
if bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupLevel) ~= 0 then
|
|
return g.OpenPopupStack.Size > 0
|
|
else
|
|
return g.OpenPopupStack.Size > g.BeginPopupStack.Size
|
|
end
|
|
else
|
|
if bit32.band(popup_flags, ImGuiPopupFlags.AnyPopupLevel) ~= 0 then
|
|
-- Return true if the popup is open anywhere in the popup stack
|
|
for n = 1, g.OpenPopupStack.Size do
|
|
if g.OpenPopupStack.Data[n].PopupId == id then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
else
|
|
-- Return true if the popup is open at the current BeginPopup() level of the popup stack (this is the most-common query)
|
|
return g.OpenPopupStack.Size > g.BeginPopupStack.Size and g.OpenPopupStack.Data[g.BeginPopupStack.Size + 1].PopupId == id
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Also see FindBlockingModal(nil)
|
|
--- @return ImGuiWindow?
|
|
function ImGui.GetTopMostPopupModal()
|
|
local g = GImGui
|
|
for n = g.OpenPopupStack.Size, 1, -1 do
|
|
local popup = g.OpenPopupStack.Data[n].Window
|
|
if popup then
|
|
if bit32.band(popup.Flags, ImGuiWindowFlags.Modal) ~= 0 then
|
|
return popup
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Initiate focusing and moving window when clicking on empty space or title bar.
|
|
-- Initiate focusing window when clicking on a disabled item.
|
|
-- Handle left-click and right-click focus.
|
|
function ImGui.UpdateMouseMovingWindowEndFrame()
|
|
local g = GImGui
|
|
|
|
if g.ActiveId ~= 0 or (g.HoveredId ~= 0 and not g.HoveredIdIsDisabled) then
|
|
return
|
|
end
|
|
|
|
-- Unless we just made a window/popup appear
|
|
if g.NavWindow and g.NavWindow.Appearing then
|
|
return
|
|
end
|
|
|
|
local hovered_window = g.HoveredWindow
|
|
|
|
-- Click on empty space to focus window and start moving
|
|
-- (after we're done with all our widgets, so e.g. clicking on docking tab-bar which have set HoveredId already and not get us here!)
|
|
if ImGui.IsMouseClickedEx(0, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner) then
|
|
-- Handle the edge case of a popup being closed while clicking in its empty space.
|
|
-- If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more.
|
|
local hovered_root
|
|
if hovered_window then
|
|
hovered_root = hovered_window.RootWindow
|
|
else
|
|
hovered_root = nil
|
|
end
|
|
local is_closed_popup = hovered_root and (bit32.band(hovered_root.Flags, ImGuiWindowFlags.Popup) ~= 0) and not ImGui.IsPopupOpen(hovered_root.PopupId, ImGuiPopupFlags.AnyPopupLevel)
|
|
local is_queued_focus_request = g.NavMoveSubmitted and (bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.FocusApi) ~= 0)
|
|
|
|
if hovered_window ~= nil and not is_closed_popup and not is_queued_focus_request then
|
|
ImGui.StartMouseMovingWindow(hovered_window)
|
|
|
|
-- FIXME: In principle we might be able to call StopMouseMovingWindow() below.
|
|
-- Please note how StartMouseMovingWindow() and StopMouseMovingWindow() and not entirely symmetrical, at the later doesn't clear ActiveId.
|
|
|
|
-- Cancel moving if clicked outside of title bar
|
|
if bit32.band(hovered_window.BgClickFlags, ImGuiWindowBgClickFlags.Move) == 0 then -- set by io.ConfigWindowsMoveFromTitleBarOnly
|
|
if bit32.band(hovered_root.Flags, ImGuiWindowFlags.NoTitleBar) == 0 then
|
|
if not hovered_root:TitleBarRect():Contains(g.IO.MouseClickedPos[0]) then
|
|
g.MovingWindow = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cancel moving if clicked over an item which was disabled or inhibited by popups
|
|
-- (when g.HoveredIdIsDisabled == true && g.HoveredId == 0 we are inhibited by popups, when g.HoveredIdIsDisabled == true && g.HoveredId != 0 we are over a disabled item)
|
|
if g.HoveredIdIsDisabled then
|
|
g.MovingWindow = nil
|
|
g.ActiveIdDisabledId = g.HoveredId
|
|
end
|
|
elseif hovered_window == nil and g.NavWindow ~= nil then
|
|
-- Clicking on void disable focus
|
|
ImGui.FocusWindow(nil, ImGuiFocusRequestFlags.UnlessBelowModal)
|
|
end
|
|
end
|
|
|
|
-- With right mouse button we close popups without changing focus based on where the mouse is aimed
|
|
-- Instead, focus will be restored to the window under the bottom-most closed popup.
|
|
-- (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger)
|
|
if g.HoveredId == 0 and ImGui.IsMouseClickedEx(1, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner) then
|
|
-- Find the top-most window between HoveredWindow and the top-most Modal Window.
|
|
-- This is where we can trim the popup stack.
|
|
local modal = ImGui.GetTopMostPopupModal()
|
|
local hovered_window_above_modal = hovered_window and (modal == nil or ImGui.IsWindowAbove(hovered_window, modal))
|
|
ImGui.ClosePopupsOverWindow(hovered_window_above_modal and hovered_window or modal, true)
|
|
end
|
|
end
|
|
|
|
-- This is called during NewFrame()->UpdateViewportsNewFrame() only.
|
|
-- Need to keep in sync with SetWindowPos()
|
|
--- @param window ImGuiWindow
|
|
--- @param delta ImVec2
|
|
function ImGui.TranslateWindow(window, delta)
|
|
ImVec2_Copy(window.Pos, window.Pos + delta)
|
|
window.ClipRect:Translate(delta)
|
|
window.OuterRectClipped:Translate(delta)
|
|
window.InnerRect:Translate(delta)
|
|
|
|
ImVec2_Copy(window.DC.CursorPos, window.DC.CursorPos + delta)
|
|
ImVec2_Copy(window.DC.CursorStartPos, window.DC.CursorStartPos + delta)
|
|
ImVec2_Copy(window.DC.CursorMaxPos, window.DC.CursorMaxPos + delta)
|
|
ImVec2_Copy(window.DC.IdealMaxPos, window.DC.IdealMaxPos + delta)
|
|
end
|
|
|
|
--- ImGui::FindWindowByID
|
|
function ImGui.FindWindowByID(id)
|
|
local g = GImGui
|
|
|
|
if not g then return end
|
|
|
|
return g.WindowsById[id]
|
|
end
|
|
|
|
--- @param name string
|
|
--- @return ImGuiWindow?
|
|
function ImGui.FindWindowByName(name)
|
|
local id = ImHashStr(name)
|
|
return ImGui.FindWindowByID(id)
|
|
end
|
|
|
|
-- Push a new Dear ImGui window to add widgets to.
|
|
-- - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair.
|
|
-- - Begin/End can be called multiple times during the frame with the same window name to append content.
|
|
-- - The window name is used as a unique identifier to preserve window information across frames (and save rudimentary information to the .ini file).
|
|
-- You can use the "##" or "###" markers to use the same label with different id, or same id with different label. See documentation at the top of this file.
|
|
-- - Return false when window is collapsed, so you can early out in your code. You always need to call ImGui.End() even if false is returned.
|
|
-- - Passing a non-nil 'open' displays a Close button on the upper-right corner of the window, the returned 'is_open' value will be false when the button is pressed.
|
|
--- @param name string
|
|
--- @param open? bool
|
|
--- @param flags? ImGuiWindowFlags
|
|
--- @return bool is_open # The updated `open` passed in
|
|
--- @return bool no_skip_items # You always need to call `ImGui.End()` even if false is returned
|
|
function ImGui.Begin(name, open, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui
|
|
local style = g.Style
|
|
|
|
IM_ASSERT(name ~= nil and name ~= "") -- Window name required
|
|
IM_ASSERT(g.WithinFrameScope) -- Forgot to call ImGui.NewFrame()
|
|
IM_ASSERT(g.FrameCountEnded ~= g.FrameCount) -- Called ImGui.Render() or ImGui.EndFrame() and haven't called ImGui.NewFrame() again yet
|
|
|
|
-- Find or create
|
|
local window = ImGui.FindWindowByName(name)
|
|
local window_just_created = (window == nil)
|
|
if window_just_created then
|
|
window = CreateNewWindow(name, flags) --- @cast window ImGuiWindow
|
|
end
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasWindowFlags) ~= 0 then
|
|
flags = bit32.band(bit32.bor(flags, g.NextWindowData.WindowFlagsSet), bit32.bnot(g.NextWindowData.WindowFlagsClear))
|
|
end
|
|
|
|
-- Automatically disable manual moving/resizing when NoInputs is set
|
|
if bit32.band(flags, ImGuiWindowFlags.NoInputs) == ImGuiWindowFlags.NoInputs then
|
|
flags = bit32.bor(flags, ImGuiWindowFlags.NoMove, ImGuiWindowFlags.NoResize)
|
|
end
|
|
|
|
local current_frame = g.FrameCount
|
|
local first_begin_of_the_frame = (window.LastFrameActive ~= current_frame)
|
|
window.IsFallbackWindow = (g.CurrentWindowStack.Size == 0 and g.WithinFrameScopeWithImplicitWindow)
|
|
|
|
-- Update the Appearing flag (note: the BeginDocked() path may also set this to true later)
|
|
local window_just_activated_by_user = (window.LastFrameActive < (current_frame - 1)) -- Not using !WasActive because the implicit "Debug" window would always toggle off->on
|
|
if bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
local popup_ref = g.OpenPopupStack.Data[g.BeginPopupStack.Size + 1]
|
|
window_just_activated_by_user = window_just_activated_by_user or (window.PopupId ~= popup_ref.PopupId) -- We recycle popups so treat window as activated if popup id changed
|
|
window_just_activated_by_user = window_just_activated_by_user or (window ~= popup_ref.Window)
|
|
end
|
|
|
|
-- Update Flags, LastFrameActive, BeginOrderXXX fields
|
|
local window_was_appearing = window.Appearing
|
|
if first_begin_of_the_frame then
|
|
ImGui.UpdateWindowInFocusOrderList(window, window_just_created, flags)
|
|
window.Appearing = window_just_activated_by_user
|
|
if (window.Appearing) then
|
|
SetWindowConditionAllowFlags(window, ImGuiCond.Appearing, true)
|
|
end
|
|
window.FlagsPreviousFrame = window.Flags
|
|
window.Flags = flags
|
|
window.ChildFlags = (bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasChildFlags) ~= 0) and g.NextWindowData.ChildFlagsSet or 0
|
|
window.LastFrameActive = current_frame
|
|
window.LastTimeActive = g.Time
|
|
window.BeginOrderWithinParent = 0
|
|
window.BeginOrderWithinContext = g.WindowsActiveCount
|
|
g.WindowsActiveCount = g.WindowsActiveCount + 1
|
|
else
|
|
flags = window.Flags
|
|
end
|
|
|
|
-- Docking
|
|
-- (NB: during the frame dock nodes are created, it is possible that (window->DockIsActive == false) even though (window->DockNode->Windows.Size > 1)
|
|
IM_ASSERT(window.DockNode == nil or window.DockNodeAsHost == nil) -- Cannot be both
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasDock) ~= 0 then
|
|
ImGui.SetWindowDock(window, g.NextWindowData.DockId, g.NextWindowData.DockCond)
|
|
end
|
|
if first_begin_of_the_frame then
|
|
local has_dock_node = (window.DockId ~= 0 or window.DockNode ~= nil)
|
|
local new_auto_dock_node = not has_dock_node and ImGui.GetWindowAlwaysWantOwnTabBar(window)
|
|
local dock_node_was_visible = window.DockNodeIsVisible
|
|
local dock_tab_was_visible = window.DockTabIsVisible
|
|
window.DockIsActive = false
|
|
window.DockNodeIsVisible = false
|
|
window.DockTabIsVisible = false
|
|
|
|
if has_dock_node or new_auto_dock_node then
|
|
open = ImGui.BeginDocked(window, open)
|
|
flags = window.Flags
|
|
if window.DockIsActive then
|
|
IM_ASSERT(window.DockNode ~= nil)
|
|
g.NextWindowData.HasFlags = bit32.band(g.NextWindowData.HasFlags, bit32.bnot(ImGuiNextWindowDataFlags.HasSizeConstraint)) -- Docking currently override constraints
|
|
end
|
|
|
|
-- Amend the Appearing flag
|
|
if window.DockTabIsVisible and not dock_tab_was_visible and dock_node_was_visible and not window.Appearing and not window_was_appearing then
|
|
window.Appearing = true
|
|
SetWindowConditionAllowFlags(window, ImGuiCond.Appearing, true)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack
|
|
local parent_window_in_stack
|
|
if window.DockIsActive and window.DockNode.HostWindow then
|
|
parent_window_in_stack = window.DockNode.HostWindow
|
|
elseif g.CurrentWindowStack:empty() then
|
|
parent_window_in_stack = nil
|
|
else
|
|
parent_window_in_stack = g.CurrentWindowStack:back().Window
|
|
end
|
|
local parent_window
|
|
if first_begin_of_the_frame then
|
|
if (bit32.band(flags, bit32.bor(ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.Popup, ImGuiWindowFlags.Tooltip)) ~= 0) then
|
|
parent_window = parent_window_in_stack
|
|
else
|
|
parent_window = nil
|
|
end
|
|
else
|
|
parent_window = window.ParentWindow
|
|
end
|
|
|
|
-- We allow window memory to be compacted so recreate the base stack when needed.
|
|
if window.IDStack.Size == 0 then
|
|
window.IDStack:push_back(window.ID)
|
|
end
|
|
|
|
-- Add to stack
|
|
g.CurrentWindow = window
|
|
g.CurrentWindowStack:resize(g.CurrentWindowStack.Size + 1)
|
|
g.CurrentWindowStack.Data[g.CurrentWindowStack.Size] = ImGuiWindowStackData()
|
|
local window_stack_data = g.CurrentWindowStack.Data[g.CurrentWindowStack.Size]
|
|
window_stack_data.Window = window
|
|
ImGuiLastItemData_Copy(window_stack_data.ParentLastItemDataBackup, g.LastItemData)
|
|
window_stack_data.DisabledOverrideReenable = (bit32.band(flags, ImGuiWindowFlags.Tooltip) ~= 0) and (bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) ~= 0)
|
|
window_stack_data.DisabledOverrideReenableAlphaBackup = 0.0
|
|
window_stack_data.StackSizesInBegin = ImGuiErrorRecoveryState()
|
|
ImGui.ErrorRecoveryStoreState(window_stack_data.StackSizesInBegin)
|
|
g.StackSizesInBeginForCurrentWindow = window_stack_data.StackSizesInBegin
|
|
if bit32.band(flags, ImGuiWindowFlags.ChildMenu) ~= 0 then
|
|
g.BeginMenuDepth = g.BeginMenuDepth + 1
|
|
end
|
|
|
|
-- Update .RootWindow and others pointers (before any possible call to FocusWindow)
|
|
if first_begin_of_the_frame then
|
|
ImGui.UpdateWindowParentAndRootLinks(window, flags, parent_window)
|
|
window.ParentWindowInBeginStack = parent_window_in_stack
|
|
|
|
-- Focus route
|
|
-- There's little point to expose a flag to set this: because the interesting cases won't be using parent_window_in_stack,
|
|
-- Use for e.g. linking a tool window in a standalone viewport to a document window, regardless of their Begin() stack parenting. (#6798)
|
|
if window.RootWindow ~= window then
|
|
window.ParentWindowForFocusRoute = parent_window_in_stack
|
|
else
|
|
window.ParentWindowForFocusRoute = nil
|
|
end
|
|
if window.ParentWindowForFocusRoute == nil and window.DockNode ~= nil then
|
|
if bit32.band(window.DockNode.MergedFlags, ImGuiDockNodeFlags.DockedWindowsInFocusRoute) ~= 0 then
|
|
window.ParentWindowForFocusRoute = window.DockNode.HostWindow
|
|
end
|
|
end
|
|
-- Override with SetNextWindowClass() field or direct call to SetWindowParentWindowForFocusRoute()
|
|
if window.WindowClass and window.WindowClass.FocusRouteParentWindowId ~= nil and window.WindowClass.FocusRouteParentWindowId ~= 0 then
|
|
window.ParentWindowForFocusRoute = ImGui.FindWindowByID(window.WindowClass.FocusRouteParentWindowId)
|
|
end
|
|
|
|
-- Inherit SetWindowFontScale() from parent until we fix this system...
|
|
if parent_window then
|
|
window.FontWindowScaleParents = parent_window.FontWindowScaleParents * parent_window.FontWindowScale
|
|
else
|
|
window.FontWindowScaleParents = 1.0
|
|
end
|
|
end
|
|
|
|
-- Add to focus scope stack
|
|
ImGui.PushFocusScope((bit32.band(window.ChildFlags, ImGuiChildFlags.NavFlattened) ~= 0) and g.CurrentFocusScopeId or window.ID)
|
|
window.NavRootFocusScopeId = g.CurrentFocusScopeId
|
|
|
|
-- Add to popup stacks: update OpenPopupStack[] data, push to BeginPopupStack[]
|
|
if bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
local popup_ref = g.OpenPopupStack.Data[g.BeginPopupStack.Size + 1]
|
|
popup_ref.Window = window
|
|
popup_ref.ParentNavLayer = parent_window_in_stack.DC.NavLayerCurrent
|
|
g.BeginPopupStack:push_back(popup_ref)
|
|
window.PopupId = popup_ref.PopupId
|
|
end
|
|
|
|
-- Process SetNextWindow***() calls
|
|
-- (FIXME: Consider splitting the HasXXX flags into X/Y components)
|
|
local window_pos_set_by_api = false
|
|
local window_size_x_set_by_api = false
|
|
local window_size_y_set_by_api = false
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasPos) ~= 0 then
|
|
window_pos_set_by_api = (bit32.band(window.SetWindowPosAllowFlags, g.NextWindowData.PosCond) ~= 0)
|
|
if window_pos_set_by_api and ImLengthSqr(g.NextWindowData.PosPivotVal) > 1e-5 then
|
|
-- May be processed on the next frame if this is our first frame and we are measuring size
|
|
-- FIXME: Look into removing the branch so everything can go through this same code path for consistency.
|
|
ImVec2_Copy(window.SetWindowPosVal, g.NextWindowData.PosVal)
|
|
ImVec2_Copy(window.SetWindowPosPivot, g.NextWindowData.PosPivotVal)
|
|
window.SetWindowPosAllowFlags = bit32.band(window.SetWindowPosAllowFlags, bit32.bnot(bit32.bor(ImGuiCond.Once, ImGuiCond.FirstUseEver, ImGuiCond.Appearing)))
|
|
else
|
|
ImGui.SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond)
|
|
end
|
|
end
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSize) ~= 0 then
|
|
window_size_x_set_by_api = (bit32.band(window.SetWindowSizeAllowFlags, g.NextWindowData.SizeCond) ~= 0) and (g.NextWindowData.SizeVal.x > 0.0)
|
|
window_size_y_set_by_api = (bit32.band(window.SetWindowSizeAllowFlags, g.NextWindowData.SizeCond) ~= 0) and (g.NextWindowData.SizeVal.y > 0.0)
|
|
if (bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeX) ~= 0 and bit32.band(window.SetWindowSizeAllowFlags, ImGuiCond.FirstUseEver) == 0) then -- Axis-specific conditions for BeginChild()
|
|
g.NextWindowData.SizeVal.x = window.SizeFull.x
|
|
end
|
|
if (bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeY) ~= 0 and bit32.band(window.SetWindowSizeAllowFlags, ImGuiCond.FirstUseEver) == 0) then
|
|
g.NextWindowData.SizeVal.y = window.SizeFull.y
|
|
end
|
|
ImGui.SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond);
|
|
end
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasScroll) ~= 0 then
|
|
if g.NextWindowData.ScrollVal.x >= 0.0 then
|
|
window.ScrollTarget.x = g.NextWindowData.ScrollVal.x
|
|
window.ScrollTargetCenterRatio.x = 0.0
|
|
end
|
|
if g.NextWindowData.ScrollVal.y >= 0.0 then
|
|
window.ScrollTarget.y = g.NextWindowData.ScrollVal.y
|
|
window.ScrollTargetCenterRatio.y = 0.0
|
|
end
|
|
end
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasContentSize) ~= 0 then
|
|
ImVec2_Copy(window.ContentSizeExplicit, g.NextWindowData.ContentSizeVal)
|
|
elseif first_begin_of_the_frame then
|
|
window.ContentSizeExplicit = ImVec2(0.0, 0.0)
|
|
end
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasWindowClass) ~= 0 then
|
|
window.WindowClass = g.NextWindowData.WindowClass
|
|
end
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasCollapsed) ~= 0 then
|
|
ImGui.SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond)
|
|
end
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasFocus) ~= 0 then
|
|
ImGui.FocusWindow(window)
|
|
end
|
|
if window.Appearing then
|
|
SetWindowConditionAllowFlags(window, ImGuiCond.Appearing, false)
|
|
end
|
|
|
|
-- [EXPERIMENTAL] Skip Refresh mode
|
|
ImGui.UpdateWindowSkipRefresh(window)
|
|
|
|
-- Nested root windows (typically tooltips) override disabled state
|
|
if window_stack_data.DisabledOverrideReenable and window.RootWindow == window then
|
|
ImGui.BeginDisabledOverrideReenable()
|
|
end
|
|
|
|
-- We intentionally set g.CurrentWindow to nil to prevent usage until when the viewport is set, then will call SetCurrentWindow()
|
|
g.CurrentWindow = nil
|
|
|
|
-- When reusing window again multiple times a frame, just append content (don't need to setup again)
|
|
if first_begin_of_the_frame and not window.SkipRefresh then
|
|
-- Initialize
|
|
local window_is_child_tooltip = (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 and bit32.band(flags, ImGuiWindowFlags.Tooltip) ~= 0) -- FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345)
|
|
local window_just_appearing_after_hidden_for_resize = (window.HiddenFramesCannotSkipItems > 0)
|
|
|
|
window.Active = true
|
|
window.HasCloseButton = (open ~= nil)
|
|
ImRect_CopyFromV4(window.ClipRect, ImVec4(-FLT_MAX, -FLT_MAX, FLT_MAX, FLT_MAX))
|
|
|
|
window.IDStack:resize(1)
|
|
|
|
window.DrawList:_ResetForNewFrame()
|
|
window.DC.CurrentTableIdx = -1
|
|
if bit32.band(flags, ImGuiWindowFlags.DockNodeHost) ~= 0 then
|
|
window.DrawList:ChannelsSplit(2)
|
|
window.DrawList:ChannelsSetCurrent(1) -- DOCKING_HOST_DRAW_CHANNEL_FG: Render decorations on channel 1 as we will render the backgrounds manually later
|
|
end
|
|
|
|
-- Restore buffer capacity when woken from a compacted state, to avoid
|
|
if window.MemoryCompacted then
|
|
ImGui.GcAwakeTransientWindowBuffers(window)
|
|
end
|
|
|
|
-- UPDATE CONTENTS SIZE, UPDATE HIDDEN STATUS
|
|
|
|
-- Update contents size from last frame for auto-fitting (or use explicit size)
|
|
CalcWindowContentSizes(window, window.ContentSize, window.ContentSizeIdeal)
|
|
|
|
-- FIXME: These flags are decremented before they are used. This means that in order to have these fields produce their intended behaviors
|
|
-- for one frame we must set them to at least 2, which is counter-intuitive. HiddenFramesCannotSkipItems is a more complicated case because
|
|
-- it has a single usage before this code block and may be set below before it is finally checked.
|
|
if window.HiddenFramesCanSkipItems > 0 then
|
|
window.HiddenFramesCanSkipItems = window.HiddenFramesCanSkipItems - 1
|
|
end
|
|
if window.HiddenFramesCannotSkipItems > 0 then
|
|
window.HiddenFramesCannotSkipItems = window.HiddenFramesCannotSkipItems - 1
|
|
end
|
|
if window.HiddenFramesForRenderOnly > 0 then
|
|
window.HiddenFramesForRenderOnly = window.HiddenFramesForRenderOnly - 1
|
|
end
|
|
|
|
-- Hide new windows for one frame until they calculate their size
|
|
if window_just_created and (not window_size_x_set_by_api or not window_size_y_set_by_api) then
|
|
window.HiddenFramesCannotSkipItems = 1
|
|
end
|
|
|
|
-- Hide popup/tooltip window when re-opening while we measure size (because we recycle the windows)
|
|
-- We reset Size/ContentSize for reappearing popups/tooltips early in this function, so further code won't be tempted to use the old size.
|
|
if window_just_activated_by_user and bit32.band(flags, bit32.bor(ImGuiWindowFlags.Popup, ImGuiWindowFlags.Tooltip)) ~= 0 then
|
|
window.HiddenFramesCannotSkipItems = 1
|
|
if bit32.band(flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0 then
|
|
if not window_size_x_set_by_api then
|
|
window.SizeFull.x = 0.0
|
|
window.Size.x = 0.0
|
|
end
|
|
if not window_size_y_set_by_api then
|
|
window.SizeFull.y = 0.0
|
|
window.Size.y = 0.0
|
|
end
|
|
ImVec2_Copy(window.ContentSize, ImVec2(0.0, 0.0))
|
|
ImVec2_Copy(window.ContentSizeIdeal, ImVec2(0.0, 0.0))
|
|
end
|
|
end
|
|
|
|
-- SELECT VIEWPORT
|
|
-- We need to do this before using any style/font sizes, as viewport with a different DPI may affect font sizes.
|
|
|
|
ImGui.WindowSelectViewport(window)
|
|
ImGui.SetCurrentViewport(window, window.Viewport)
|
|
SetCurrentWindow(window)
|
|
flags = window.Flags
|
|
|
|
-- LOCK BORDER SIZE AND PADDING FOR THE FRAME (so that altering them doesn't cause inconsistencies)
|
|
-- We read Style data after the call to UpdateSelectWindowViewport() which might be swapping the style.
|
|
|
|
if not window.DockIsActive and bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
window.WindowBorderSize = style.ChildBorderSize
|
|
else
|
|
window.WindowBorderSize = (bit32.band(flags, bit32.bor(ImGuiWindowFlags.Popup, ImGuiWindowFlags.Tooltip)) ~= 0 and bit32.band(flags, ImGuiWindowFlags.Modal) == 0) and style.PopupBorderSize or style.WindowBorderSize
|
|
end
|
|
window.WindowPadding = style.WindowPadding
|
|
if not window.DockIsActive and (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0) and (bit32.band(flags, ImGuiWindowFlags.Popup) == 0) and (bit32.band(window.ChildFlags, ImGuiChildFlags.AlwaysUseWindowPadding) == 0) and window.WindowBorderSize == 0.0 then
|
|
if bit32.band(flags, ImGuiWindowFlags.MenuBar) ~= 0 then
|
|
window.WindowPadding = ImVec2(0.0, style.WindowPadding.y)
|
|
else
|
|
window.WindowPadding = ImVec2(0.0, 0.0)
|
|
end
|
|
end
|
|
|
|
-- Lock menu offset so size calculation can use it as menu-bar windows need a minimum size.
|
|
window.DC.MenuBarOffset.x = ImMax(ImMax(window.WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x)
|
|
window.DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y
|
|
|
|
if bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0 then
|
|
window.TitleBarHeight = 0.0
|
|
else
|
|
window.TitleBarHeight = g.FontSize + g.Style.FramePadding.y * 2.0
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiWindowFlags.MenuBar) ~= 0 then
|
|
window.MenuBarHeight = window.DC.MenuBarOffset.y + g.FontSize + g.Style.FramePadding.y * 2.0
|
|
else
|
|
window.MenuBarHeight = 0.0
|
|
end
|
|
|
|
window.TitleBarHeight = (bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) and 0 or g.FontSize + g.Style.FramePadding.y * 2
|
|
|
|
window.FontRefSize = g.FontSize -- Lock this to discourage calling window:CalcFontSize() outside of current window.
|
|
|
|
-- Depending on condition we use previous or current window size to compare against contents size to decide if a scrollbar should be visible.
|
|
-- Those flags will be altered further down in the function depending on more conditions.
|
|
local use_current_size_for_scrollbar_x = window_just_created
|
|
local use_current_size_for_scrollbar_y = window_just_created
|
|
if window_size_x_set_by_api and window.ContentSizeExplicit.x ~= 0.0 then
|
|
use_current_size_for_scrollbar_x = true
|
|
end
|
|
if window_size_y_set_by_api and window.ContentSizeExplicit.y ~= 0.0 then -- #7252
|
|
use_current_size_for_scrollbar_y = true
|
|
end
|
|
|
|
-- Collapse window by double-clicking on title bar
|
|
-- At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing
|
|
if bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0 and bit32.band(flags, ImGuiWindowFlags.NoCollapse) == 0 and not window.DockIsActive then
|
|
-- We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed),
|
|
-- so verify that we don't have items over the title bar.
|
|
local title_bar_rect = window:TitleBarRect()
|
|
if g.HoveredWindow == window and g.HoveredId == 0 and g.HoveredIdPreviousFrame == 0 and g.ActiveId == 0 and ImGui.IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) then
|
|
if g.IO.MouseClickedCount[0] == 2 and ImGui.GetKeyOwner(ImGuiKey.MouseLeft) == ImGuiKeyOwner_NoOwner then
|
|
window.WantCollapseToggle = true
|
|
ImGui.SetKeyOwner(ImGuiKey.MouseLeft, window.MoveId) -- Claim input the same way ButtonBehavior() does. Prevent a same-frame move from triggering other items. (#9439)
|
|
end
|
|
end
|
|
if window.WantCollapseToggle then
|
|
window.Collapsed = not window.Collapsed
|
|
if not window.Collapsed then
|
|
use_current_size_for_scrollbar_y = true
|
|
end
|
|
ImGui.MarkIniSettingsDirty(window)
|
|
end
|
|
else
|
|
window.Collapsed = false
|
|
end
|
|
window.WantCollapseToggle = false
|
|
|
|
-- SIZE
|
|
|
|
-- Outer Decoration Sizes
|
|
-- (we need to clear ScrollbarSize immediately as CalcWindowAutoFitSize() needs it and can be called from other locations).
|
|
local scrollbar_sizes_from_last_frame = ImVec2()
|
|
ImVec2_Copy(scrollbar_sizes_from_last_frame, window.ScrollbarSizes)
|
|
|
|
window.DecoOuterSizeX1 = 0.0
|
|
window.DecoOuterSizeX2 = 0.0
|
|
window.DecoOuterSizeY1 = window.TitleBarHeight + window.MenuBarHeight
|
|
window.DecoOuterSizeY2 = 0.0
|
|
window.ScrollbarSizes = ImVec2(0.0, 0.0)
|
|
|
|
-- Calculate auto-fit size, handle automatic resize
|
|
-- - Using SetNextWindowSize() overrides ImGuiWindowFlags.AlwaysAutoResize, so it can be used on tooltips/popups, etc.
|
|
-- - We still process initial auto-fit on collapsed windows to get a window width, but otherwise don't honor ImGuiWindowFlags.AlwaysAutoResize when collapsed.
|
|
-- - Auto-fit may only grow window during the first few frames.
|
|
do
|
|
local size_auto_fit_x_always = not window_size_x_set_by_api and (bit32.band(flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0) and not window.Collapsed
|
|
local size_auto_fit_y_always = not window_size_y_set_by_api and (bit32.band(flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0) and not window.Collapsed
|
|
local size_auto_fit_x_current = not window_size_x_set_by_api and (window.AutoFitFramesX > 0)
|
|
local size_auto_fit_y_current = not window_size_y_set_by_api and (window.AutoFitFramesY > 0)
|
|
|
|
local size_auto_fit_mask = 0
|
|
if size_auto_fit_x_always or size_auto_fit_x_current then
|
|
size_auto_fit_mask = bit32.bor(size_auto_fit_mask, bit32.lshift(1, ImGuiAxis.X - 1))
|
|
end
|
|
if size_auto_fit_y_always or size_auto_fit_y_current then
|
|
size_auto_fit_mask = bit32.bor(size_auto_fit_mask, bit32.lshift(1, ImGuiAxis.Y - 1))
|
|
end
|
|
|
|
local size_auto_fit = CalcWindowAutoFitSize(window, window.ContentSizeIdeal, size_auto_fit_mask)
|
|
local old_size = ImVec2(window.SizeFull.x, window.SizeFull.y)
|
|
|
|
if size_auto_fit_x_always or size_auto_fit_x_current then
|
|
if size_auto_fit_x_always then
|
|
window.SizeFull.x = size_auto_fit.x
|
|
else
|
|
if window.AutoFitOnlyGrows then
|
|
window.SizeFull.x = ImMax(window.SizeFull.x, size_auto_fit.x)
|
|
else
|
|
window.SizeFull.x = size_auto_fit.x
|
|
end
|
|
end
|
|
use_current_size_for_scrollbar_x = true
|
|
end
|
|
if size_auto_fit_y_always or size_auto_fit_y_current then
|
|
if size_auto_fit_y_always then
|
|
window.SizeFull.y = size_auto_fit.y
|
|
else
|
|
if window.AutoFitOnlyGrows then
|
|
window.SizeFull.y = ImMax(window.SizeFull.y, size_auto_fit.y)
|
|
else
|
|
window.SizeFull.y = size_auto_fit.y
|
|
end
|
|
end
|
|
use_current_size_for_scrollbar_y = true
|
|
end
|
|
|
|
if old_size.x ~= window.SizeFull.x or old_size.y ~= window.SizeFull.y then
|
|
ImGui.MarkIniSettingsDirty(window)
|
|
end
|
|
end
|
|
|
|
-- Apply minimum/maximum window size constraints and final size
|
|
ImVec2_Copy(window.SizeFull, CalcWindowSizeAfterConstraint(window, window.SizeFull))
|
|
ImVec2_Copy(window.Size, (window.Collapsed and bit32.band(flags, ImGuiWindowFlags.ChildWindow) == 0) and window:TitleBarRect():GetSize() or window.SizeFull)
|
|
|
|
-- POSITION
|
|
|
|
-- Popup latch its initial position, will position itself when it appears next frame
|
|
if window_just_activated_by_user then
|
|
window.AutoPosLastDirection = ImGuiDir.None
|
|
if bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0 and bit32.band(flags, ImGuiWindowFlags.Modal) == 0 and not window_pos_set_by_api then -- FIXME: BeginPopup() could use SetNextWindowPos()
|
|
ImVec2_Copy(window.Pos, g.BeginPopupStack:back().OpenPopupPos)
|
|
end
|
|
end
|
|
|
|
-- Position child window
|
|
if bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
IM_ASSERT(parent_window and parent_window.Active)
|
|
window.BeginOrderWithinParent = parent_window.DC.ChildWindows.Size
|
|
parent_window.DC.ChildWindows:push_back(window)
|
|
if (bit32.band(flags, ImGuiWindowFlags.Popup) == 0) and not window_pos_set_by_api and not window_is_child_tooltip then
|
|
ImVec2_Copy(window.Pos, parent_window.DC.CursorPos)
|
|
end
|
|
end
|
|
|
|
local window_pos_with_pivot = (window.SetWindowPosVal.x ~= FLT_MAX and window.HiddenFramesCannotSkipItems == 0)
|
|
if window_pos_with_pivot then
|
|
ImGui.SetWindowPos(window, window.SetWindowPosVal - window.Size * window.SetWindowPosPivot, 0) -- Position given a pivot (e.g. for centering)
|
|
elseif (bit32.band(flags, ImGuiWindowFlags.ChildMenu) ~= 0) then
|
|
ImVec2_Copy(window.Pos, ImGui.FindBestWindowPosForPopup(window))
|
|
elseif (bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0) and not window_pos_set_by_api and window_just_appearing_after_hidden_for_resize then
|
|
ImVec2_Copy(window.Pos, ImGui.FindBestWindowPosForPopup(window))
|
|
elseif (bit32.band(flags, ImGuiWindowFlags.Tooltip) ~= 0) and not window_pos_set_by_api and not window_is_child_tooltip then
|
|
ImVec2_Copy(window.Pos, ImGui.FindBestWindowPosForPopup(window))
|
|
end
|
|
|
|
-- Late create viewport if we don't fit within our current host viewport.
|
|
if window.ViewportAllowPlatformMonitorExtend >= 0 and not window.ViewportOwned and bit32.band(window.Viewport.Flags, ImGuiViewportFlags.IsMinimized) == 0 then
|
|
if not window.Viewport:GetMainRect():Contains(window:Rect()) then
|
|
-- This is based on the assumption that the DPI will be known ahead (same as the DPI of the selection done in UpdateSelectWindowViewport)
|
|
-- local old_viewport = window.Viewport
|
|
window.Viewport = ImGui.AddUpdateViewport(window, window.ID, window.Pos, window.Size, ImGuiViewportFlags.NoFocusOnAppearing)
|
|
|
|
-- FIXME-DPI
|
|
-- IM_ASSERT(old_viewport.DpiScale == window.Viewport.DpiScale) -- FIXME-DPI: Something went wrong
|
|
ImGui.SetCurrentViewport(window, window.Viewport)
|
|
SetCurrentWindow(window)
|
|
end
|
|
end
|
|
|
|
if window.ViewportOwned then
|
|
ImGui.WindowSyncOwnedViewport(window, parent_window_in_stack)
|
|
end
|
|
|
|
-- Calculate the range of allowed position for that window (to be movable and visible past safe area padding)
|
|
-- When clamping to stay visible, we will enforce that window.Pos stays inside of visibility_rect.
|
|
local viewport_rect = window.Viewport:GetMainRect()
|
|
local viewport_work_rect = window.Viewport:GetWorkRect()
|
|
local visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding)
|
|
local visibility_rect = ImRect(viewport_work_rect.Min + visibility_padding, viewport_work_rect.Max - visibility_padding)
|
|
|
|
-- Clamp position/size so window stays visible within its viewport or monitor
|
|
-- Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing.
|
|
-- FIXME: Similar to code in GetWindowAllowedExtentRect()
|
|
if not window_pos_set_by_api and bit32.band(flags, ImGuiWindowFlags.ChildWindow) == 0 then
|
|
if not window.ViewportOwned and viewport_rect:GetWidth() > 0 and viewport_rect:GetHeight() > 0.0 then
|
|
local size_for_clamping_y = window.Size.y
|
|
if bit32.band(window.BgClickFlags, ImGuiWindowBgClickFlags.Move) == 0 and bit32.band(window.Flags, ImGuiWindowFlags.NoTitleBar) == 0 then
|
|
size_for_clamping_y = window.TitleBarHeight
|
|
end
|
|
window.Pos.x = ImClamp(window.Pos.x, visibility_rect.Min.x - window.Size.x, visibility_rect.Max.x)
|
|
window.Pos.y = ImClamp(window.Pos.y, visibility_rect.Min.y - size_for_clamping_y, visibility_rect.Max.y)
|
|
end
|
|
end
|
|
window.Pos.x = ImTrunc(window.Pos.x) window.Pos.y = ImTrunc(window.Pos.y)
|
|
|
|
-- Apply window focus (new and reactivated windows are moved to front)
|
|
local want_focus = false
|
|
if (window_just_activated_by_user and bit32.band(flags, ImGuiWindowFlags.NoFocusOnAppearing) == 0) then
|
|
if bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
want_focus = true
|
|
elseif (window.DockIsActive or bit32.band(flags, ImGuiWindowFlags.ChildWindow) == 0) and bit32.band(flags, ImGuiWindowFlags.Tooltip) == 0 then
|
|
want_focus = true
|
|
end
|
|
end
|
|
|
|
-- Lock window rounding for the frame (so that altering them doesn't cause inconsistencies)
|
|
-- Large values tend to lead to variety of artifacts and are not recommended.
|
|
if bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 and not window.DockIsActive then
|
|
window.WindowRounding = style.ChildRounding
|
|
elseif window.RootWindowDockTree.ViewportOwned then
|
|
window.WindowRounding = 0.0
|
|
else
|
|
if (bit32.band(flags, ImGuiWindowFlags.Popup) ~= 0 and bit32.band(flags, ImGuiWindowFlags.Modal) == 0) then
|
|
window.WindowRounding = style.PopupRounding
|
|
else
|
|
window.WindowRounding = style.WindowRounding
|
|
end
|
|
end
|
|
|
|
-- Decide if we are going to handle borders and resize grips
|
|
-- 'window.SkipItems' is not updated yet so for child windows we rely on ParentWindow to avoid submitting decorations. (#8815)
|
|
-- Whenever we add support for full decorated child windows we will likely make this logic more general.
|
|
local handle_borders_and_resize_grips = (window.DockNodeAsHost ~= nil or not window.DockIsActive)
|
|
if bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 and window.ParentWindow.SkipItems then
|
|
handle_borders_and_resize_grips = false
|
|
end
|
|
|
|
-- Handle manual resize: Resize Grips, Borders, Gamepad
|
|
-- Child windows can only be resized when they have the flags set. The resize grip allows resizing in both directions, so it should appear only if both flags are set.
|
|
local border_hovered, border_held = -1, -1
|
|
local resize_grip_col = {0, 0, 0, 0}
|
|
|
|
local resize_grip_count
|
|
if (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0) and (bit32.band(flags, ImGuiWindowFlags.Popup) == 0) then
|
|
if (bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeX) ~= 0) and (bit32.band(window.ChildFlags, ImGuiChildFlags.ResizeY) ~= 0) then
|
|
resize_grip_count = 1
|
|
else
|
|
resize_grip_count = 0
|
|
end
|
|
else
|
|
resize_grip_count = g.IO.ConfigWindowsResizeFromEdges and 2 or 1 -- Allow resize from lower-left if we have the mouse cursor feedback for it.
|
|
end
|
|
|
|
local resize_grip_draw_size = ImTrunc(ImMax(g.FontSize * 1.10, g.Style.WindowRounding + 1.0 + g.FontSize * 0.2))
|
|
if handle_borders_and_resize_grips and not window.Collapsed then
|
|
local auto_fit_mask
|
|
auto_fit_mask, border_hovered, border_held = UpdateWindowManualResize(window, resize_grip_count, resize_grip_col, visibility_rect)
|
|
if auto_fit_mask ~= 0 then
|
|
if bit32.band(auto_fit_mask, bit32.lshift(1, ImGuiAxis.X - 1)) ~= 0 then
|
|
use_current_size_for_scrollbar_x = true
|
|
end
|
|
if bit32.band(auto_fit_mask, bit32.lshift(1, ImGuiAxis.Y - 1)) ~= 0 then
|
|
use_current_size_for_scrollbar_y = true
|
|
end
|
|
end
|
|
end
|
|
window.ResizeBorderHovered = border_hovered
|
|
window.ResizeBorderHeld = border_held
|
|
|
|
-- Synchronize window --> viewport again and one last time (clamping and manual resize may have affected either)
|
|
if window.ViewportOwned then
|
|
if not window.Viewport.PlatformRequestMove then
|
|
ImVec2_Copy(window.Viewport.Pos, window.Pos)
|
|
end
|
|
if not window.Viewport.PlatformRequestResize then
|
|
ImVec2_Copy(window.Viewport.Size, window.Size)
|
|
end
|
|
window.Viewport:UpdateWorkRect()
|
|
viewport_rect = window.Viewport:GetMainRect()
|
|
end
|
|
|
|
-- Save last known viewport position within the window itself (so it can be saved in .ini file and restored)
|
|
ImVec2_Copy(window.ViewportPos, window.Viewport.Pos)
|
|
|
|
-- SCROLLBAR VISIBILITY
|
|
|
|
-- Update scrollbar visibility (based on the Size that was effective during last frame or the auto-resized Size).
|
|
if not window.Collapsed then
|
|
-- When reading the current size we need to read it after size constraints have been applied.
|
|
-- Intentionally use previous frame values for InnerRect and ScrollbarSizes.
|
|
-- And when we use window.DecorationUp here it doesn't have ScrollbarSizes.y applied yet.
|
|
local avail_size_from_current_frame = ImVec2(window.SizeFull.x, window.SizeFull.y - (window.DecoOuterSizeY1 + window.DecoOuterSizeY2))
|
|
local avail_size_from_last_frame = window.InnerRect:GetSize() + scrollbar_sizes_from_last_frame
|
|
local needed_size_from_last_frame = window_just_created and ImVec2(0, 0) or window.ContentSize + window.WindowPadding * 2.0
|
|
|
|
local size_for_scrollbars_x = use_current_size_for_scrollbar_x and avail_size_from_current_frame.x or avail_size_from_last_frame.x
|
|
local size_for_scrollbars_y = use_current_size_for_scrollbar_y and avail_size_from_current_frame.y or avail_size_from_last_frame.y
|
|
|
|
local scrollbar_x_prev = window.ScrollbarX
|
|
-- local scrollbar_y_from_last_frame = window.ScrollbarY -- FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons?
|
|
|
|
window.ScrollbarY = (bit32.band(flags, ImGuiWindowFlags.AlwaysVerticalScrollbar) ~= 0) or ((needed_size_from_last_frame.y > size_for_scrollbars_y) and not (bit32.band(flags, ImGuiWindowFlags.NoScrollbar) ~= 0))
|
|
window.ScrollbarX = (bit32.band(flags, ImGuiWindowFlags.AlwaysHorizontalScrollbar) ~= 0) or ((needed_size_from_last_frame.x > size_for_scrollbars_x - (window.ScrollbarY and style.ScrollbarSize or 0.0)) and not (bit32.band(flags, ImGuiWindowFlags.NoScrollbar) ~= 0) and (bit32.band(flags, ImGuiWindowFlags.HorizontalScrollbar) ~= 0))
|
|
|
|
-- Track when ScrollbarX visibility keeps toggling, which is a sign of a feedback loop, and stabilize by enforcing visibility (#3285, #8488)
|
|
-- (Feedback loops of this sort can manifest in various situations, but combining horizontal + vertical scrollbar + using a clipper with varying width items is one frequent cause.
|
|
-- The better solution is to, either (1) enforce visibility by using ImGuiWindowFlags.AlwaysHorizontalScrollbar or (2) declare stable contents width with SetNextWindowContentSize(), if you can compute it)
|
|
window.ScrollbarXStabilizeToggledHistory = bit32.lshift(window.ScrollbarXStabilizeToggledHistory, 1)
|
|
if scrollbar_x_prev ~= window.ScrollbarX then
|
|
window.ScrollbarXStabilizeToggledHistory = bit32.bor(window.ScrollbarXStabilizeToggledHistory, 0x01)
|
|
end
|
|
|
|
local scrollbar_x_stabilize = (window.ScrollbarXStabilizeToggledHistory ~= 0) and ImCountSetBits(window.ScrollbarXStabilizeToggledHistory) >= 4 -- 4 == half of bits in our U8 history.
|
|
if scrollbar_x_stabilize then
|
|
window.ScrollbarX = true
|
|
end
|
|
|
|
-- if scrollbar_x_stabilize and not window.ScrollbarXStabilizeEnabled then
|
|
-- IMGUI_DEBUG_LOG("[scroll] Stabilize ScrollbarX for Window '%s'\n", window.Name)
|
|
-- end
|
|
window.ScrollbarXStabilizeEnabled = scrollbar_x_stabilize
|
|
|
|
if window.ScrollbarX and not window.ScrollbarY then
|
|
window.ScrollbarY = (needed_size_from_last_frame.y > size_for_scrollbars_y - style.ScrollbarSize) and not (bit32.band(flags, ImGuiWindowFlags.NoScrollbar) ~= 0)
|
|
end
|
|
|
|
window.ScrollbarSizes = ImVec2(window.ScrollbarY and style.ScrollbarSize or 0.0, window.ScrollbarX and style.ScrollbarSize or 0.0)
|
|
|
|
-- Amend the partially filled window.DecorationXXX values.
|
|
window.DecoOuterSizeX2 = window.DecoOuterSizeX2 + window.ScrollbarSizes.x
|
|
window.DecoOuterSizeY2 = window.DecoOuterSizeY2 + window.ScrollbarSizes.y
|
|
end
|
|
|
|
-- UPDATE RECTANGLES (1- THOSE NOT AFFECTED BY SCROLLING)
|
|
-- Update various regions. Variables they depend on should be set above in this function.
|
|
-- We set this up after processing the resize grip so that our rectangles doesn't lag by a frame.
|
|
|
|
-- Outer rectangle
|
|
-- Not affected by window border size. Used by:
|
|
-- - FindHoveredWindow() (w/ extra padding when border resize is enabled)
|
|
-- - Begin() initial clipping rect for drawing window background and borders.
|
|
-- - Begin() clipping whole child
|
|
local host_rect
|
|
if (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 and bit32.band(flags, ImGuiWindowFlags.Popup) == 0 and not window_is_child_tooltip) then
|
|
host_rect = parent_window.ClipRect
|
|
else
|
|
host_rect = viewport_rect
|
|
end
|
|
local outer_rect = window:Rect()
|
|
local title_bar_rect = window:TitleBarRect()
|
|
ImRect_Copy(window.OuterRectClipped, outer_rect)
|
|
if window.DockIsActive then
|
|
window.OuterRectClipped.Min.y = window.OuterRectClipped.Min.y + window.TitleBarHeight
|
|
end
|
|
window.OuterRectClipped:ClipWith(host_rect)
|
|
|
|
-- Inner rectangle
|
|
-- Not affected by window border size. Used by:
|
|
-- - InnerClipRect
|
|
-- - ScrollToRectEx()
|
|
-- - NavUpdatePageUpPageDown()
|
|
-- - Scrollbar()
|
|
window.InnerRect.Min.x = window.Pos.x + window.DecoOuterSizeX1
|
|
window.InnerRect.Min.y = window.Pos.y + window.DecoOuterSizeY1
|
|
window.InnerRect.Max.x = window.Pos.x + window.Size.x - window.DecoOuterSizeX2
|
|
window.InnerRect.Max.y = window.Pos.y + window.Size.y - window.DecoOuterSizeY2
|
|
|
|
-- Inner clipping rectangle.
|
|
-- - Extend a outside of normal work region up to borders.
|
|
-- - This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space.
|
|
-- - It also makes clipped items be more noticeable.
|
|
-- - And is consistent on both axis (prior to 2024/05/03 ClipRect used WindowPadding.x * 0.5 on left and right edge), see #3312
|
|
-- - Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result.
|
|
-- Note that if our window is collapsed we will end up with an inverted (~null) clipping rectangle which is the correct behavior.
|
|
-- Affected by window/frame border size. Used by:
|
|
-- - Begin() initial clip rect
|
|
local top_border_size = ((bit32.band(flags, ImGuiWindowFlags.MenuBar) ~= 0 or bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0) and style.FrameBorderSize or window.WindowBorderSize)
|
|
|
|
-- Try to match the fact that our border is drawn centered over the window rectangle, rather than inner.
|
|
-- This is why we do a *0.5 here. We don't currently even technically support large values for WindowBorderSize,
|
|
-- see e.g #7887 #7888, but may do after we move the window border to become an inner border (and then we can remove the 0.5 here).
|
|
window.InnerClipRect.Min.x = ImFloor(0.5 + window.InnerRect.Min.x + window.WindowBorderSize * 0.5)
|
|
window.InnerClipRect.Min.y = ImFloor(0.5 + window.InnerRect.Min.y + top_border_size * 0.5)
|
|
window.InnerClipRect.Max.x = ImFloor(window.InnerRect.Max.x - window.WindowBorderSize * 0.5)
|
|
window.InnerClipRect.Max.y = ImFloor(window.InnerRect.Max.y - window.WindowBorderSize * 0.5)
|
|
window.InnerClipRect:ClipWithFull(host_rect)
|
|
|
|
-- SCROLLING
|
|
|
|
-- Lock down maximum scrolling
|
|
-- The value of ScrollMax are ahead from ScrollbarX/ScrollbarY which is intentionally using InnerRect from previous rect in order to accommodate
|
|
-- for right/bottom aligned items without creating a scrollbar.
|
|
window.ScrollMax.x = ImMax(0.0, window.ContentSize.x + window.WindowPadding.x * 2.0 - window.InnerRect:GetWidth())
|
|
window.ScrollMax.y = ImMax(0.0, window.ContentSize.y + window.WindowPadding.y * 2.0 - window.InnerRect:GetHeight())
|
|
|
|
-- Apply scrolling
|
|
ImVec2_Copy(window.Scroll, CalcNextScrollFromScrollTargetAndClamp(window))
|
|
ImVec2_CopyV(window.ScrollTarget, FLT_MAX, FLT_MAX)
|
|
window.DecoInnerSizeX1 = 0.0; window.DecoInnerSizeY1 = 0.0
|
|
|
|
-- DRAWING
|
|
|
|
-- Setup draw list and outer clipping rectangle
|
|
IM_ASSERT(window.DrawList.CmdBuffer.Size == 1 and window.DrawList.CmdBuffer.Data[1].ElemCount == 0)
|
|
window.DrawList:PushTexture(g.Font.OwnerAtlas.TexRef)
|
|
ImGui.PushClipRect(host_rect.Min, host_rect.Max, false)
|
|
|
|
-- Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71)
|
|
-- When using overlapping child windows, this will break the assumption that child z-order is mapped to submission order.
|
|
-- FIXME: User code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected (github #4493)
|
|
local is_undocked_or_docked_visible = not window.DockIsActive or window.DockTabIsVisible
|
|
if is_undocked_or_docked_visible then
|
|
local render_decorations_in_parent = false
|
|
if (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0) and (bit32.band(flags, ImGuiWindowFlags.Popup) == 0) and not window_is_child_tooltip then
|
|
-- - We test overlap with the previous child window only (testing all would end up being O(log N) not a good investment here)
|
|
-- - We disable this when the parent window has zero vertices, which is a common pattern leading to laying out multiple overlapping childs
|
|
local previous_child
|
|
if parent_window.DC.ChildWindows.Size >= 2 then
|
|
previous_child = parent_window.DC.ChildWindows.Data[parent_window.DC.ChildWindows.Size - 1]
|
|
else
|
|
previous_child = nil
|
|
end
|
|
local previous_child_overlapping
|
|
if previous_child ~= nil then
|
|
previous_child_overlapping = previous_child:Rect():Overlaps(window:Rect())
|
|
else
|
|
previous_child_overlapping = false
|
|
end
|
|
local parent_is_empty = (parent_window.DrawList.VtxBuffer.Size == 0)
|
|
if (window.DrawList.CmdBuffer:back().ElemCount == 0 and not parent_is_empty and not previous_child_overlapping) then
|
|
render_decorations_in_parent = true
|
|
end
|
|
end
|
|
|
|
if render_decorations_in_parent then
|
|
window.DrawList = parent_window.DrawList
|
|
end
|
|
|
|
-- Handle title bar, scrollbar, resize grips and resize borders
|
|
local window_to_highlight = g.NavWindowingTarget or g.NavWindow
|
|
local title_bar_is_highlight = want_focus or (window_to_highlight ~= nil and (window.RootWindowForTitleBarHighlight == window_to_highlight.RootWindowForTitleBarHighlight or (window.DockNode ~= nil and window.DockNode == window_to_highlight.DockNode)))
|
|
|
|
RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_col, resize_grip_draw_size)
|
|
|
|
if render_decorations_in_parent then
|
|
window.DrawList = window.DrawListInst
|
|
end
|
|
end
|
|
|
|
-- UPDATE RECTANGLES (2- THOSE AFFECTED BY SCROLLING)
|
|
|
|
-- Work rectangle.
|
|
-- Affected by window padding and border size. Used by:
|
|
-- - Columns() for right-most edge
|
|
-- - TreeNode(), CollapsingHeader() for right-most edge
|
|
-- - BeginTabBar() for right-most edge
|
|
local allow_scrollbar_x = (bit32.band(flags, ImGuiWindowFlags.NoScrollbar) == 0) and (bit32.band(flags, ImGuiWindowFlags.HorizontalScrollbar) ~= 0)
|
|
local allow_scrollbar_y = (bit32.band(flags, ImGuiWindowFlags.NoScrollbar) == 0)
|
|
|
|
local work_rect_size_x
|
|
if window.ContentSizeExplicit.x ~= 0.0 then
|
|
work_rect_size_x = window.ContentSizeExplicit.x
|
|
else
|
|
local content_size_x = allow_scrollbar_x and (window.ContentSize and window.ContentSize.x or 0.0) or 0.0
|
|
local window_size_x = window.Size.x - window.WindowPadding.x * 2.0 - (window.DecoOuterSizeX1 + window.DecoOuterSizeX2)
|
|
work_rect_size_x = ImMax(content_size_x, window_size_x)
|
|
end
|
|
|
|
local work_rect_size_y
|
|
if window.ContentSizeExplicit.y ~= 0.0 then
|
|
work_rect_size_y = window.ContentSizeExplicit.y
|
|
else
|
|
local content_size_y = allow_scrollbar_y and (window.ContentSize and window.ContentSize.y or 0.0) or 0.0
|
|
local window_size_y = window.Size.y - window.WindowPadding.y * 2.0 - (window.DecoOuterSizeY1 + window.DecoOuterSizeY2)
|
|
work_rect_size_y = ImMax(content_size_y, window_size_y)
|
|
end
|
|
|
|
window.WorkRect.Min.x = ImTrunc(window.InnerRect.Min.x - window.Scroll.x + ImMax(window.WindowPadding.x, window.WindowBorderSize))
|
|
window.WorkRect.Min.y = ImTrunc(window.InnerRect.Min.y - window.Scroll.y + ImMax(window.WindowPadding.y, window.WindowBorderSize))
|
|
window.WorkRect.Max.x = window.WorkRect.Min.x + work_rect_size_x
|
|
window.WorkRect.Max.y = window.WorkRect.Min.y + work_rect_size_y
|
|
window.ParentWorkRect = window.WorkRect
|
|
|
|
-- [LEGACY] Content Region
|
|
-- FIXME-OBSOLETE: window.ContentRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it.
|
|
-- Unless explicit content size is specified by user, this currently represent the region leading to no scrolling.
|
|
-- Used by:
|
|
-- - Mouse wheel scrolling + many other things
|
|
window.ContentRegionRect.Min.x = window.Pos.x - window.Scroll.x + window.WindowPadding.x + window.DecoOuterSizeX1
|
|
window.ContentRegionRect.Min.y = window.Pos.y - window.Scroll.y + window.WindowPadding.y + window.DecoOuterSizeY1
|
|
window.ContentRegionRect.Max.x = window.ContentRegionRect.Min.x + (window.ContentSizeExplicit.x ~= 0.0 and window.ContentSizeExplicit.x or (window.Size.x - window.WindowPadding.x * 2.0 - (window.DecoOuterSizeX1 + window.DecoOuterSizeX2)))
|
|
window.ContentRegionRect.Max.y = window.ContentRegionRect.Min.y + (window.ContentSizeExplicit.y ~= 0.0 and window.ContentSizeExplicit.y or (window.Size.y - window.WindowPadding.y * 2.0 - (window.DecoOuterSizeY1 + window.DecoOuterSizeY2)))
|
|
|
|
-- Setup drawing context
|
|
-- (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window. and window.DC. is dubious.)
|
|
window.DC.Indent.x = window.DecoOuterSizeX1 + window.WindowPadding.x - window.Scroll.x
|
|
window.DC.GroupOffset.x = 0.0
|
|
window.DC.ColumnsOffset.x = 0.0
|
|
|
|
-- Record the loss of precision of CursorStartPos which can happen due to really large scrolling amount.
|
|
-- This is used by clipper to compensate and fix the most common use case of large scroll area. Easy and cheap, next best thing compared to switching everything to double or ImU64.
|
|
local start_pos_highp_x = window.Pos.x + window.WindowPadding.x - window.Scroll.x + window.DecoOuterSizeX1 + window.DC.ColumnsOffset.x
|
|
local start_pos_highp_y = window.Pos.y + window.WindowPadding.y - window.Scroll.y + window.DecoOuterSizeY1
|
|
ImVec2_CopyV(window.DC.CursorStartPos, start_pos_highp_x, start_pos_highp_y)
|
|
ImVec2_CopyV(window.DC.CursorStartPosLossyness, start_pos_highp_x - window.DC.CursorStartPos.x, start_pos_highp_y - window.DC.CursorStartPos.y)
|
|
ImVec2_Copy(window.DC.CursorPos, window.DC.CursorStartPos)
|
|
ImVec2_Copy(window.DC.CursorPosPrevLine, window.DC.CursorPos)
|
|
ImVec2_Copy(window.DC.CursorMaxPos, window.DC.CursorStartPos)
|
|
ImVec2_Copy(window.DC.IdealMaxPos, window.DC.CursorStartPos)
|
|
ImVec2_CopyV(window.DC.CurrLineSize, 0.0, 0.0)
|
|
ImVec2_CopyV(window.DC.PrevLineSize, 0.0, 0.0)
|
|
window.DC.CurrLineTextBaseOffset = 0.0
|
|
window.DC.PrevLineTextBaseOffset = 0.0
|
|
window.DC.IsSameLine = false
|
|
window.DC.IsSetPos = false
|
|
|
|
window.DC.NavLayerCurrent = ImGuiNavLayer.Main
|
|
window.DC.NavLayersActiveMask = window.DC.NavLayersActiveMaskNext
|
|
window.DC.NavLayersActiveMaskNext = 0x00
|
|
window.DC.NavIsScrollPushableX = true
|
|
window.DC.NavHideHighlightOneFrame = false
|
|
window.DC.NavWindowHasScrollY = window.ScrollMax.y > 0.0
|
|
|
|
window.DC.MenuBarAppending = false
|
|
window.DC.MenuColumns:Update(style.ItemSpacing.x, window_just_activated_by_user)
|
|
window.DC.TreeDepth = 0
|
|
window.DC.TreeHasStackDataDepthMask = 0x00
|
|
window.DC.TreeRecordsClippedNodesY2Mask = 0x00
|
|
window.DC.ChildWindows:resize(0)
|
|
window.DC.StateStorage = window.StateStorage
|
|
window.DC.CurrentColumns = nil
|
|
window.DC.LayoutType = ImGuiLayoutType.Vertical
|
|
window.DC.ParentLayoutType = (parent_window ~= nil) and parent_window.DC.LayoutType or ImGuiLayoutType.Vertical
|
|
|
|
-- Default item width. Make it proportional to window size if window can be manually resized.
|
|
-- (we cannot use AutoFitFramesX/AutoFitFramesY which is a temporary state)
|
|
local is_resizable_width
|
|
if bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 and not window.DockIsActive then
|
|
is_resizable_width = (window.Size.x > 0.0) and (bit32.band(window.ChildFlags, bit32.bor(ImGuiChildFlags.AutoResizeX, ImGuiChildFlags.AlwaysAutoResize)) == 0)
|
|
else
|
|
is_resizable_width = (window.Size.x > 0.0) and (bit32.band(flags, ImGuiWindowFlags.AlwaysAutoResize) == 0)
|
|
end
|
|
|
|
if is_resizable_width then
|
|
window.DC.ItemWidthDefault = ImTrunc(window.Size.x * 0.65)
|
|
else
|
|
window.DC.ItemWidthDefault = ImTrunc(g.FontSize * 16.0)
|
|
end
|
|
window.DC.ItemWidth = window.DC.ItemWidthDefault
|
|
window.DC.ItemWidthStack:resize(0)
|
|
window.DC.TextWrapPos = -1.0 -- Disabled
|
|
window.DC.TextWrapPosStack:resize(0)
|
|
if bit32.band(flags, ImGuiWindowFlags.Modal) ~= 0 then
|
|
window.DC.ModalDimBgColor = ImGui.ColorConvertFloat4ToU32(ImGui.GetStyleColorVec4(ImGuiCol.ModalWindowDimBg))
|
|
end
|
|
|
|
if window.AutoFitFramesX > 0 then
|
|
window.AutoFitFramesX = window.AutoFitFramesX - 1
|
|
end
|
|
if window.AutoFitFramesY > 0 then
|
|
window.AutoFitFramesY = window.AutoFitFramesY - 1
|
|
end
|
|
|
|
-- Clear SetNextWindowXXX data (can aim to move this higher in the function)
|
|
g.NextWindowData:ClearFlags()
|
|
|
|
-- Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there)
|
|
-- We ImGuiFocusRequestFlags.UnlessBelowModal to:
|
|
-- - Avoid focusing a window that is created outside of a modal. This will prevent active modal from being closed.
|
|
-- - Position window behind the modal that is not a begin-parent of this window.
|
|
if want_focus then
|
|
ImGui.FocusWindow(window, ImGuiFocusRequestFlags.UnlessBelowModal)
|
|
end
|
|
if want_focus and window == g.NavWindow then
|
|
ImGui.NavInitWindow(window, false) -- <-- this is in the way for us to be able to defer and sort reappearing FocusWindow() calls
|
|
end
|
|
|
|
-- Pressing Ctrl+C copy window content into the clipboard
|
|
-- [EXPERIMENTAL] Breaks on nested Begin/End pairs. We need to work that out and add better logging scope.
|
|
-- [EXPERIMENTAL] Text outputs has many issues.
|
|
if g.IO.ConfigWindowsCopyContentsWithCtrlC then
|
|
if g.NavWindow and g.NavWindow.RootWindow == window and g.ActiveId == 0 and ImGui.Shortcut(bit32.bor(ImGuiMod_Ctrl, ImGuiKey.C)) then
|
|
ImGui.LogToClipboard(0)
|
|
end
|
|
end
|
|
|
|
-- Title bar
|
|
if bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0 and not window.DockIsActive then
|
|
open = RenderWindowTitleBarContents(window, ImRect(title_bar_rect.Min.x + window.WindowBorderSize, title_bar_rect.Min.y, title_bar_rect.Max.x - window.WindowBorderSize, title_bar_rect.Max.y), name, open)
|
|
elseif bit32.band(flags, ImGuiWindowFlags.NoTitleBar) == 0 and window.DockIsActive then
|
|
ImGui.LogText("%s\n", string.sub(window.Name, 1, ImGui.FindRenderedTextEnd(window.Name) - 1))
|
|
end
|
|
|
|
-- Clear hit test shape every frame
|
|
window.HitTestHoleSize.x = 0; window.HitTestHoleSize.y = 0
|
|
|
|
if bit32.band(flags, ImGuiWindowFlags.Tooltip) ~= 0 then
|
|
g.TooltipPreviousWindow = window
|
|
end
|
|
|
|
if bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.DockingEnable) ~= 0 then
|
|
-- Docking: Dragging a dockable window (or any of its child) turns it into a drag and drop source.
|
|
-- We need to do this _before_ we overwrite window.DC.LastItemId below because BeginDockableDragDropSource() also overwrites it.
|
|
if g.MovingWindow == window and bit32.band(window.RootWindowDockTree.Flags, ImGuiWindowFlags.NoDocking) == 0 then
|
|
ImGui.BeginDockableDragDropSource(window)
|
|
end
|
|
|
|
-- Docking: Any dockable window can act as a target. For dock node hosts we call BeginDockableDragDropTarget() in DockNodeUpdate() instead.
|
|
if g.DragDropActive and bit32.band(flags, ImGuiWindowFlags.NoDocking) == 0 then
|
|
if g.MovingWindow == nil or g.MovingWindow.RootWindowDockTree ~= window then
|
|
if window == window.RootWindowDockTree and bit32.band(window.Flags, ImGuiWindowFlags.DockNodeHost) == 0 then
|
|
ImGui.BeginDockableDragDropTarget(window)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Set default BgClickFlags
|
|
-- This is set at the end of this function, so UpdateWindowManualResize()/ClampWindowPos() may use last-frame value if overridden by user code.
|
|
-- FIXME: The general intent is that we will later expose config options to default to enable scrolling + select scrolling mouse button.
|
|
if bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
window.BgClickFlags = parent_window.BgClickFlags
|
|
else
|
|
window.BgClickFlags = g.IO.ConfigWindowsMoveFromTitleBarOnly and ImGuiWindowBgClickFlags.None or ImGuiWindowBgClickFlags.Move
|
|
end
|
|
|
|
-- We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin().
|
|
-- This is useful to allow creating context menus on title bar only, etc.
|
|
window.DC.WindowItemStatusFlags = ImGuiItemStatusFlags.None
|
|
|
|
if ImGui.IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) then
|
|
window.DC.WindowItemStatusFlags = bit32.bor(window.DC.WindowItemStatusFlags, ImGuiItemStatusFlags.HoveredRect)
|
|
end
|
|
ImGui.SetLastItemDataForWindow(window, title_bar_rect)
|
|
else
|
|
-- Skip refresh always mark active
|
|
if (window.SkipRefresh) then
|
|
SetWindowActiveForSkipRefresh(window)
|
|
end
|
|
|
|
-- Append
|
|
ImGui.SetCurrentViewport(window, window.Viewport)
|
|
SetCurrentWindow(window)
|
|
g.NextWindowData:ClearFlags()
|
|
ImGui.SetLastItemDataForWindow(window, window:TitleBarRect())
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiWindowFlags.DockNodeHost) == 0 and not window.SkipRefresh then
|
|
ImGui.PushClipRect(window.InnerClipRect.Min, window.InnerClipRect.Max, true)
|
|
-- [Roblox backend] content clip + scroll origin: lets the backend move scrolled content instead of redrawing it
|
|
local dl, cr = window.DrawList, window.DrawList._CmdHeader.ClipRect
|
|
dl._ContentClipX0, dl._ContentClipY0, dl._ContentClipX1, dl._ContentClipY1 = cr.x, cr.y, cr.z, cr.w
|
|
dl._ScrollOriginX, dl._ScrollOriginY = window.Pos.x - window.Scroll.x, window.Pos.y - window.Scroll.y
|
|
end
|
|
|
|
-- Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused)
|
|
window.WriteAccessed = false
|
|
window.BeginCount = window.BeginCount + 1
|
|
g.NextWindowData:ClearFlags()
|
|
|
|
-- Update visibility
|
|
if first_begin_of_the_frame and not window.SkipRefresh then
|
|
-- When we are about to select this tab (which will only be visible on the _next frame_), flag it with a non-zero HiddenFramesCannotSkipItems.
|
|
-- This will have the important effect of actually returning true in Begin() and not setting SkipItems, allowing an earlier submission of the window contents.
|
|
-- This is analogous to regular windows being hidden from one frame.
|
|
-- It is especially important as e.g. nested TabBars would otherwise generate flicker in the form of one empty frame, or focus requests won't be processed.
|
|
if window.DockIsActive and not window.DockTabIsVisible then
|
|
if window.LastFrameJustFocused == g.FrameCount then
|
|
window.HiddenFramesCannotSkipItems = 1
|
|
else
|
|
window.HiddenFramesCanSkipItems = 1
|
|
end
|
|
end
|
|
|
|
if (bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0) and (bit32.band(flags, ImGuiWindowFlags.ChildMenu) == 0) then
|
|
-- Child window can be out of sight and have "negative" clip windows.
|
|
-- Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar).
|
|
IM_ASSERT((bit32.band(flags, ImGuiWindowFlags.NoTitleBar) ~= 0) or window.DockIsActive)
|
|
|
|
local nav_request = (bit32.band(window.ChildFlags, ImGuiChildFlags.NavFlattened) ~= 0) and (g.NavAnyRequest and g.NavWindow and g.NavWindow.RootWindowForNav == window.RootWindowForNav)
|
|
|
|
if not g.LogEnabled and not nav_request then
|
|
if window.OuterRectClipped.Min.x >= window.OuterRectClipped.Max.x or window.OuterRectClipped.Min.y >= window.OuterRectClipped.Max.y then
|
|
if window.AutoFitFramesX > 0 or window.AutoFitFramesY > 0 then
|
|
window.HiddenFramesCannotSkipItems = 1
|
|
else
|
|
window.HiddenFramesCanSkipItems = 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Hide along with parent or if parent is collapsed
|
|
if parent_window and (parent_window.Collapsed or parent_window.HiddenFramesCanSkipItems > 0) then
|
|
window.HiddenFramesCanSkipItems = 1
|
|
end
|
|
if parent_window and parent_window.HiddenFramesCannotSkipItems > 0 then
|
|
window.HiddenFramesCannotSkipItems = 1
|
|
end
|
|
end
|
|
|
|
-- Don't render if style alpha is 0.0 at the time of Begin(). This is arbitrary and inconsistent but has been there for a long while (may remove at some point)
|
|
if style.Alpha <= 0.0 then
|
|
window.HiddenFramesCanSkipItems = 1
|
|
end
|
|
|
|
-- Update the Hidden flag
|
|
local hidden_regular = (window.HiddenFramesCanSkipItems > 0) or (window.HiddenFramesCannotSkipItems > 0)
|
|
window.Hidden = hidden_regular or (window.HiddenFramesForRenderOnly > 0)
|
|
|
|
-- Disable inputs for requested number of frames
|
|
if window.DisableInputsFrames > 0 then
|
|
window.DisableInputsFrames = window.DisableInputsFrames - 1
|
|
window.Flags = bit32.bor(window.Flags, ImGuiWindowFlags.NoInputs)
|
|
end
|
|
|
|
-- Update the SkipItems flag, used to early out of all items functions (no layout required)
|
|
local skip_items = false
|
|
if window.Collapsed or not window.Active or hidden_regular then
|
|
if window.AutoFitFramesX <= 0 and window.AutoFitFramesY <= 0 and window.HiddenFramesCannotSkipItems <= 0 then
|
|
skip_items = true
|
|
end
|
|
end
|
|
window.SkipItems = skip_items
|
|
|
|
-- Restore NavLayersActiveMaskNext to previous value when not visible, so a CTRL+Tab back can use a safe value.
|
|
if window.SkipItems then
|
|
window.DC.NavLayersActiveMaskNext = window.DC.NavLayersActiveMask
|
|
end
|
|
elseif first_begin_of_the_frame then
|
|
-- Skip refresh mode
|
|
window.SkipItems = true
|
|
end
|
|
|
|
if open == nil then open = true end
|
|
return open, not window.SkipItems
|
|
end
|
|
|
|
function ImGui.End()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
-- Error checking: verify that user hasn't called End() too many times!
|
|
if (g.CurrentWindowStack.Size <= 1 and g.WithinFrameScopeWithImplicitWindow) then
|
|
IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size > 1, "Calling End() too many times!")
|
|
|
|
return
|
|
end
|
|
local window_stack_data = g.CurrentWindowStack:back()
|
|
|
|
-- Error checking: verify that user doesn't directly call End() on a child window.
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
IM_ASSERT_USER_ERROR(g.WithinEndPopupID == window.ID, "Must call EndPopup() and not End()!")
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 and bit32.band(window.Flags, ImGuiWindowFlags.DockNodeHost) == 0 and not window.DockIsActive then
|
|
IM_ASSERT_USER_ERROR(g.WithinEndChildID == window.ID, "Must call EndChild() and not End()!")
|
|
end
|
|
|
|
-- Close anything that is open
|
|
if window.DC.CurrentColumns then
|
|
ImGui.EndColumns()
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.DockNodeHost) == 0 and not window.SkipRefresh then -- Pop inner window clip rectangle
|
|
ImGui.PopClipRect()
|
|
end
|
|
ImGui.PopFocusScope()
|
|
|
|
if window_stack_data.DisabledOverrideReenable and window.RootWindow == window then
|
|
ImGui.EndDisabledOverrideReenable()
|
|
end
|
|
|
|
if (window.SkipRefresh) then
|
|
IM_ASSERT(window.DrawList == nil)
|
|
window.DrawList = window.DrawListInst
|
|
end
|
|
if window.CacheSkipItemsBackup ~= nil then -- [Port] SetNextWindowCached()
|
|
window.SkipItems = window.CacheSkipItemsBackup
|
|
window.CacheSkipItemsBackup = nil
|
|
end
|
|
|
|
-- Stop logging
|
|
if g.LogWindow == window then -- FIXME: add more options for scope of logging
|
|
ImGui.LogFinish()
|
|
end
|
|
|
|
if window.DC.IsSetPos then
|
|
ImGui.ErrorCheckUsingSetCursorPosToExtendParentBoundaries()
|
|
end
|
|
|
|
-- Docking: report contents sizes to parent to allow for auto-resize
|
|
if window.DockNode and window.DockTabIsVisible then
|
|
local host_window = window.DockNode.HostWindow -- FIXME-DOCK
|
|
if host_window then
|
|
ImVec2_Copy(host_window.DC.CursorMaxPos, window.DC.CursorMaxPos + window.WindowPadding - host_window.WindowPadding)
|
|
end
|
|
end
|
|
|
|
-- Pop from window stack
|
|
ImGuiLastItemData_Copy(g.LastItemData, window_stack_data.ParentLastItemDataBackup)
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildMenu) ~= 0 then
|
|
g.BeginMenuDepth = g.BeginMenuDepth - 1
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
g.BeginPopupStack:pop_back()
|
|
end
|
|
|
|
-- Error handling, state recovery
|
|
if g.IO.ConfigErrorRecovery and window_stack_data.StackSizesInBegin then
|
|
ImGui.ErrorRecoveryTryToRecoverWindowState(window_stack_data.StackSizesInBegin)
|
|
end
|
|
|
|
g.CurrentWindowStack:pop_back()
|
|
|
|
-- LUA: No "Ternary Operator", since lua (... and (1) or (2)) will eval (1) and (2) no matter what
|
|
-- so something like `SetCurrentWindow((g.CurrentWindowStack.Size == 0) and nil or g.CurrentWindowStack:back().Window)` will error
|
|
if (g.CurrentWindowStack.Size == 0) then
|
|
SetCurrentWindow(nil)
|
|
else
|
|
SetCurrentWindow(g.CurrentWindowStack:back().Window)
|
|
end
|
|
if g.CurrentWindow then
|
|
ImGui.SetCurrentViewport(g.CurrentWindow, g.CurrentWindow.Viewport)
|
|
end
|
|
end
|
|
|
|
--- @param option ImGuiItemFlags
|
|
--- @param enabled bool
|
|
function ImGui.PushItemFlag(option, enabled)
|
|
local g = GImGui
|
|
local item_flags = g.CurrentItemFlags
|
|
IM_ASSERT(item_flags == g.ItemFlagsStack:back())
|
|
|
|
if enabled then
|
|
item_flags = bit32.bor(item_flags, option)
|
|
else
|
|
item_flags = bit32.band(item_flags, bit32.bnot(option))
|
|
end
|
|
|
|
g.CurrentItemFlags = item_flags
|
|
g.ItemFlagsStack:push_back(item_flags)
|
|
end
|
|
|
|
function ImGui.PopItemFlag()
|
|
local g = GImGui
|
|
IM_ASSERT_USER_ERROR_RET(g.ItemFlagsStack.Size > 1, "Calling PopItemFlag() too many times!")
|
|
g.ItemFlagsStack:pop_back()
|
|
g.CurrentItemFlags = g.ItemFlagsStack:back()
|
|
end
|
|
|
|
-- BeginDisabled()/EndDisabled()
|
|
-- - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled)
|
|
-- - Visually this is currently altering alpha, but it is expected that in a future styling system this would work differently.
|
|
-- - Feedback welcome at https://github.com/ocornut/imgui/issues/211
|
|
-- - BeginDisabled(false)/EndDisabled() essentially does nothing but is provided to facilitate use of boolean expressions.
|
|
-- (as a micro-optimization: if you have tens of thousands of BeginDisabled(false)/EndDisabled() pairs, you might want to reformulate your code to avoid making those calls)
|
|
-- - Note: mixing up BeginDisabled() and PushItemFlag(ImGuiItemFlags.Disabled) is currently NOT SUPPORTED.
|
|
--- @param disabled? bool
|
|
function ImGui.BeginDisabled(disabled)
|
|
if disabled == nil then disabled = true end
|
|
|
|
local g = GImGui
|
|
local was_disabled = bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) ~= 0
|
|
if not was_disabled and disabled then
|
|
g.DisabledAlphaBackup = g.Style.Alpha
|
|
g.Style.Alpha = g.Style.Alpha * g.Style.DisabledAlpha -- PushStyleVar(ImGuiStyleVar.Alpha, g.Style.Alpha * g.Style.DisabledAlpha)
|
|
end
|
|
if was_disabled or disabled then
|
|
g.CurrentItemFlags = bit32.bor(g.CurrentItemFlags, ImGuiItemFlags.Disabled)
|
|
end
|
|
g.ItemFlagsStack:push_back(g.CurrentItemFlags) -- FIXME-OPT: can we simply skip this and use DisabledStackSize?
|
|
g.DisabledStackSize = g.DisabledStackSize + 1
|
|
end
|
|
|
|
function ImGui.EndDisabled()
|
|
local g = GImGui
|
|
IM_ASSERT_USER_ERROR_RET(g.DisabledStackSize > 0, "Calling EndDisabled() too many times!")
|
|
g.DisabledStackSize = g.DisabledStackSize - 1
|
|
local was_disabled = bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) ~= 0
|
|
-- PopItemFlag()
|
|
g.ItemFlagsStack:pop_back()
|
|
g.CurrentItemFlags = g.ItemFlagsStack:back()
|
|
if was_disabled and bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) == 0 then
|
|
g.Style.Alpha = g.DisabledAlphaBackup -- PopStyleVar()
|
|
end
|
|
end
|
|
|
|
-- Could have been called BeginDisabledDisable() but it didn't want to be award nominated for most awkward function name.
|
|
-- Ideally we would use a shared e.g. BeginDisabled()->BeginDisabledEx() but earlier needs to be optimal.
|
|
-- The whole code for this is awkward, will reevaluate if we find a way to implement SetNextItemDisabled().
|
|
function ImGui.BeginDisabledOverrideReenable()
|
|
local g = GImGui
|
|
IM_ASSERT(bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) ~= 0)
|
|
|
|
g.CurrentWindowStack.Data[g.CurrentWindowStack.Size].DisabledOverrideReenableAlphaBackup = g.Style.Alpha
|
|
g.Style.Alpha = g.DisabledAlphaBackup
|
|
g.CurrentItemFlags = bit32.band(g.CurrentItemFlags, bit32.bnot(ImGuiItemFlags.Disabled))
|
|
g.ItemFlagsStack:push_back(g.CurrentItemFlags)
|
|
g.DisabledStackSize = g.DisabledStackSize + 1
|
|
end
|
|
|
|
function ImGui.EndDisabledOverrideReenable()
|
|
local g = GImGui
|
|
IM_ASSERT(g.DisabledStackSize > 0)
|
|
g.DisabledStackSize = g.DisabledStackSize - 1
|
|
g.ItemFlagsStack:pop_back()
|
|
g.CurrentItemFlags = g.ItemFlagsStack:back()
|
|
g.Style.Alpha = g.CurrentWindowStack:back().DisabledOverrideReenableAlphaBackup
|
|
end
|
|
|
|
-- Find window given position, search front-to-back
|
|
-- - Typically write output back to g.HoveredWindow and g.HoveredWindowUnderMovingWindow.
|
|
-- - FIXME: Note that we have an inconsequential lag here: OuterRectClipped is updated in Begin(), so windows moved programmatically
|
|
-- with SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is
|
|
-- called, aka before the next Begin(). Moving window isn't affected.
|
|
-- - The 'find_first_and_in_any_viewport = true' mode is only used by TestEngine. It is simpler to maintain here.
|
|
--- @param pos ImVec2
|
|
--- @param find_first_and_in_any_viewport bool
|
|
--- @return ImGuiWindow, ImGuiWindow?
|
|
local function FindHoveredWindowEx(pos, find_first_and_in_any_viewport)
|
|
local g = GImGui
|
|
local hovered_window = nil
|
|
local hovered_window_under_moving_window = nil
|
|
|
|
-- Special handling for the window being moved: Ignore the mouse viewport check (because it may reset/lose its viewport during the undocking frame)
|
|
hovered_window = g.MovingWindow
|
|
local backup_moving_window_viewport = nil
|
|
if (find_first_and_in_any_viewport == false and g.MovingWindow) then
|
|
backup_moving_window_viewport = g.MovingWindow.Viewport
|
|
g.MovingWindow.Viewport = g.MouseViewport
|
|
if (bit32.band(g.MovingWindow.Flags, ImGuiWindowFlags.NoMouseInputs) == 0) then
|
|
hovered_window = g.MovingWindow
|
|
end
|
|
end
|
|
|
|
local padding_regular = g.Style.TouchExtraPadding
|
|
local padding_for_resize = ImMax(g.Style.TouchExtraPadding, ImVec2(g.Style.WindowBorderHoverPadding, g.Style.WindowBorderHoverPadding))
|
|
|
|
local window
|
|
local stop_6075 = false
|
|
for i = g.Windows.Size, 1, -1 do
|
|
repeat
|
|
window = g.Windows.Data[i]
|
|
if not window.WasActive or window.Hidden then
|
|
break
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoMouseInputs) ~= 0 then
|
|
break
|
|
end
|
|
|
|
IM_ASSERT(window.Viewport)
|
|
if (window.Viewport ~= g.MouseViewport) then
|
|
break
|
|
end
|
|
|
|
-- Using the clipped AABB, a child window will typically be clipped by its parent (not always)
|
|
local hit_padding
|
|
if bit32.band(window.Flags, bit32.bor(ImGuiWindowFlags.NoResize, ImGuiWindowFlags.AlwaysAutoResize)) ~= 0 then
|
|
hit_padding = padding_regular
|
|
else
|
|
hit_padding = padding_for_resize
|
|
end
|
|
if not window.OuterRectClipped:ContainsWithPad(pos, hit_padding) then
|
|
break
|
|
end
|
|
|
|
-- Support for one rectangular hole in any given window
|
|
-- FIXME: Consider generalizing hit-testing override (with more generic data, callback, etc.) (#1512)
|
|
if window.HitTestHoleSize.x ~= 0 then
|
|
local hx, hy = window.Pos.x + window.HitTestHoleOffset.x, window.Pos.y + window.HitTestHoleOffset.y
|
|
if pos.x >= hx and pos.y >= hy and pos.x < hx + window.HitTestHoleSize.x and pos.y < hy + window.HitTestHoleSize.y then
|
|
break -- continue
|
|
end
|
|
end
|
|
|
|
if find_first_and_in_any_viewport then
|
|
hovered_window = window
|
|
stop_6075 = true; break
|
|
else
|
|
if hovered_window == nil then
|
|
hovered_window = window
|
|
end
|
|
|
|
if hovered_window_under_moving_window == nil and (not g.MovingWindow or window.RootWindowDockTree ~= g.MovingWindow.RootWindowDockTree) then
|
|
hovered_window_under_moving_window = window
|
|
end
|
|
|
|
if hovered_window and hovered_window_under_moving_window then
|
|
stop_6075 = true; break
|
|
end
|
|
end
|
|
|
|
until true
|
|
if stop_6075 then break end
|
|
end
|
|
|
|
if (find_first_and_in_any_viewport == false and g.MovingWindow) then
|
|
g.MovingWindow.Viewport = backup_moving_window_viewport
|
|
end
|
|
|
|
return hovered_window, hovered_window_under_moving_window
|
|
end
|
|
|
|
-- TODO:
|
|
-- The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app)
|
|
function ImGui.UpdateHoveredWindowAndCaptureFlags(mouse_pos)
|
|
local g = GImGui
|
|
local io = g.IO
|
|
|
|
-- FIXME-DPI: This storage was added on 2021/03/31 for test engine, but if we want to multiply WINDOWS_HOVER_PADDING
|
|
-- by DpiScale, we need to make this window-agnostic anyhow, maybe need storing inside ImGuiWindow.
|
|
g.WindowsBorderHoverPadding = ImMax(ImMax(g.Style.TouchExtraPadding.x, g.Style.TouchExtraPadding.y), g.Style.WindowBorderHoverPadding)
|
|
|
|
-- Find the window hovered by mouse:
|
|
-- - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow.
|
|
-- - When moving a window we can skip the search, which also conveniently bypasses the fact that window.WindowRectClipped is lagging as this point of the frame.
|
|
-- - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms.
|
|
local clear_hovered_windows = false
|
|
g.HoveredWindow, g.HoveredWindowUnderMovingWindow = FindHoveredWindowEx(mouse_pos, false)
|
|
IM_ASSERT(g.HoveredWindow == nil or g.HoveredWindow == g.MovingWindow or g.HoveredWindow.Viewport == g.MouseViewport)
|
|
g.HoveredWindowBeforeClear = g.HoveredWindow
|
|
|
|
-- Modal windows prevents mouse from hovering behind them.
|
|
local modal_window = ImGui.GetTopMostPopupModal()
|
|
if modal_window and g.HoveredWindow and not ImGui.IsWindowWithinBeginStackOf(g.HoveredWindow.RootWindow, modal_window) then -- FIXME-MERGE: RootWindowDockTree ?
|
|
clear_hovered_windows = true
|
|
end
|
|
|
|
-- Disabled mouse hovering (we don't currently clear MousePos, we could)
|
|
if bit32.band(io.ConfigFlags, ImGuiConfigFlags.NoMouse) ~= 0 then
|
|
clear_hovered_windows = true
|
|
end
|
|
|
|
-- We track click ownership. When clicked outside of a window the click is owned by the application and
|
|
-- won't report hovering nor request capture even while dragging over our windows afterward.
|
|
local has_open_popup = (g.OpenPopupStack.Size > 0)
|
|
local has_open_modal = (modal_window ~= nil)
|
|
local mouse_earliest_down = -1
|
|
local mouse_any_down = false
|
|
|
|
for i = 0, 2 do -- IM_COUNTOF(io.MouseDown)
|
|
if io.MouseClicked[i] then
|
|
io.MouseDownOwned[i] = (g.HoveredWindow ~= nil) or has_open_popup
|
|
io.MouseDownOwnedUnlessPopupClose[i] = (g.HoveredWindow ~= nil) or has_open_modal
|
|
end
|
|
|
|
mouse_any_down = mouse_any_down or io.MouseDown[i]
|
|
if (io.MouseDown[i] or io.MouseReleased[i]) then -- Increase release frame for our evaluation of earliest button (#1392)
|
|
if (mouse_earliest_down == -1 or (io.MouseClickedTime[i] < io.MouseClickedTime[mouse_earliest_down])) then
|
|
mouse_earliest_down = i
|
|
end
|
|
end
|
|
end
|
|
|
|
local mouse_avail = (mouse_earliest_down == -1) or io.MouseDownOwned[mouse_earliest_down]
|
|
local mouse_avail_unless_popup_close = (mouse_earliest_down == -1) or io.MouseDownOwnedUnlessPopupClose[mouse_earliest_down]
|
|
|
|
-- If mouse was first clicked outside of ImGui bounds we also cancel out hovering.
|
|
-- FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02)
|
|
local mouse_dragging_extern_payload = g.DragDropActive and bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.SourceExtern) ~= 0
|
|
if not mouse_avail and not mouse_dragging_extern_payload then
|
|
clear_hovered_windows = true
|
|
end
|
|
|
|
if clear_hovered_windows then
|
|
g.HoveredWindow = nil
|
|
g.HoveredWindowUnderMovingWindow = nil
|
|
end
|
|
|
|
-- Update io.WantCaptureMouse for the user application (true = dispatch mouse info to Dear ImGui only, false = dispatch mouse to Dear ImGui + underlying app)
|
|
-- Update io.WantCaptureMouseAllowPopupClose (experimental) to give a chance for app to react to popup closure with a drag
|
|
if (g.WantCaptureMouseNextFrame ~= -1) then
|
|
io.WantCaptureMouse = (g.WantCaptureMouseNextFrame ~= 0)
|
|
io.WantCaptureMouseUnlessPopupClose = io.WantCaptureMouse
|
|
else
|
|
io.WantCaptureMouse = (mouse_avail and (g.HoveredWindow ~= nil or mouse_any_down)) or has_open_popup
|
|
io.WantCaptureMouseUnlessPopupClose = (mouse_avail_unless_popup_close and (g.HoveredWindow ~= nil or mouse_any_down)) or has_open_modal
|
|
end
|
|
|
|
-- Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to Dear ImGui only, false = dispatch keyboard info to Dear ImGui + underlying app)
|
|
io.WantCaptureKeyboard = false
|
|
if bit32.band(io.ConfigFlags, ImGuiConfigFlags.NoKeyboard) == 0 then
|
|
if (g.ActiveId ~= 0) or (modal_window ~= nil) then
|
|
io.WantCaptureKeyboard = true
|
|
elseif io.NavActive and bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0 and io.ConfigNavCaptureKeyboard then
|
|
io.WantCaptureKeyboard = true
|
|
end
|
|
end
|
|
if g.WantCaptureKeyboardNextFrame ~= -1 then -- Manual override
|
|
io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame ~= 0)
|
|
end
|
|
|
|
-- Update io.WantTextInput flag, this is to allow systems without a keyboard (e.g. mobile, hand-held) to show a software keyboard if possible
|
|
io.WantTextInput = (g.WantTextInputNextFrame ~= -1) and (g.WantTextInputNextFrame ~= 0) or false
|
|
end
|
|
|
|
--- @param key ImGuiKey
|
|
--- @param v bool
|
|
--- @param analog_value float
|
|
function ImGui.UpdateAliasKey(key, v, analog_value)
|
|
IM_ASSERT(ImGui.IsAliasKey(key))
|
|
local key_data = ImGui.GetKeyData(nil, key)
|
|
key_data.Down = v
|
|
key_data.AnalogValue = analog_value
|
|
end
|
|
|
|
-- [Internal] Do not use directly
|
|
function ImGui.GetMergedModsFromKeys()
|
|
local g = GImGui
|
|
-- Bypass IsKeyDown() for the unlikely case where user used a ImGuiInputFlags.LockXXXX on those.
|
|
local mods = 0
|
|
|
|
if ImGui.GetKeyData(g, ImGuiMod_Ctrl).Down then mods = bit32.bor(mods, ImGuiMod_Ctrl) end
|
|
if ImGui.GetKeyData(g, ImGuiMod_Shift).Down then mods = bit32.bor(mods, ImGuiMod_Shift) end
|
|
if ImGui.GetKeyData(g, ImGuiMod_Alt).Down then mods = bit32.bor(mods, ImGuiMod_Alt) end
|
|
if ImGui.GetKeyData(g, ImGuiMod_Super).Down then mods = bit32.bor(mods, ImGuiMod_Super) end
|
|
|
|
return mods
|
|
end
|
|
|
|
function ImGui.UpdateKeyboardInputs()
|
|
local g = GImGui
|
|
local io = g.IO
|
|
|
|
if bit32.band(io.ConfigFlags, ImGuiConfigFlags.NoKeyboard) ~= 0 then
|
|
io:ClearInputKeys()
|
|
end
|
|
|
|
-- Update aliases
|
|
for n = 0, 2 do -- TODO: ImGuiMouseButton.COUNT - 1
|
|
ImGui.UpdateAliasKey(ImGui.MouseButtonToKey(n), io.MouseDown[n], io.MouseDown[n] and 1.0 or 0.0)
|
|
end
|
|
ImGui.UpdateAliasKey(ImGuiKey.MouseWheelX, io.MouseWheelH ~= 0.0, io.MouseWheelH)
|
|
ImGui.UpdateAliasKey(ImGuiKey.MouseWheelY, io.MouseWheel ~= 0.0, io.MouseWheel)
|
|
|
|
-- Synchronize io.KeyMods and io.KeyCtrl/io.KeyShift/etc. values.
|
|
-- - New backends (1.87+): send io.AddKeyEvent(ImGuiMod_XXX) -> -> (here) deriving io.KeyMods + io.KeyXXX from key array.
|
|
-- - Legacy backends: set io.KeyXXX bools -> (above) set key array from io.KeyXXX -> (here) deriving io.KeyMods + io.KeyXXX from key array.
|
|
-- So with legacy backends the 4 values will do a unnecessary back-and-forth but it makes the code simpler and future facing.
|
|
local prev_key_mods = io.KeyMods
|
|
io.KeyMods = ImGui.GetMergedModsFromKeys()
|
|
io.KeyCtrl = bit32.band(io.KeyMods, ImGuiMod_Ctrl) ~= 0
|
|
io.KeyShift = bit32.band(io.KeyMods, ImGuiMod_Shift) ~= 0
|
|
io.KeyAlt = bit32.band(io.KeyMods, ImGuiMod_Alt) ~= 0
|
|
io.KeySuper = bit32.band(io.KeyMods, ImGuiMod_Super) ~= 0
|
|
if prev_key_mods ~= io.KeyMods then
|
|
g.LastKeyModsChangeTime = g.Time
|
|
end
|
|
if prev_key_mods ~= io.KeyMods and prev_key_mods == 0 then
|
|
g.LastKeyModsChangeFromNoneTime = g.Time
|
|
end
|
|
|
|
-- Clear gamepad data if disabled
|
|
if bit32.band(io.BackendFlags, ImGuiBackendFlags.HasGamepad) == 0 then
|
|
for key = ImGuiKey_Gamepad_BEGIN, ImGuiKey_Gamepad_END - 1 do
|
|
io.KeysData[key - ImGuiKey.NamedKey_BEGIN].Down = false
|
|
io.KeysData[key - ImGuiKey.NamedKey_BEGIN].AnalogValue = 0.0
|
|
end
|
|
end
|
|
|
|
-- Update keys
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
local key_data = io.KeysData[key - ImGuiKey.NamedKey_BEGIN]
|
|
key_data.DownDurationPrev = key_data.DownDuration
|
|
if key_data.Down then
|
|
if key_data.DownDuration < 0.0 then
|
|
key_data.DownDuration = 0.0
|
|
else
|
|
key_data.DownDuration = key_data.DownDuration + io.DeltaTime
|
|
end
|
|
else
|
|
key_data.DownDuration = -1.0
|
|
end
|
|
if key_data.DownDuration == 0.0 then
|
|
if ImGui.IsKeyboardKey(key) then
|
|
g.LastKeyboardKeyPressTime = g.Time
|
|
elseif key == ImGuiKey.ReservedForModCtrl or key == ImGuiKey.ReservedForModShift or key == ImGuiKey.ReservedForModAlt or key == ImGuiKey.ReservedForModSuper then
|
|
g.LastKeyboardKeyPressTime = g.Time
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update keys/input owner (named keys only): one entry per key
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
local key_data = io.KeysData[key - ImGuiKey.NamedKey_BEGIN]
|
|
local owner_data = g.KeysOwnerData[key - ImGuiKey.NamedKey_BEGIN]
|
|
owner_data.OwnerCurr = owner_data.OwnerNext
|
|
if not key_data.Down then -- Important: ownership is released on the frame after a release. Ensure a 'MouseDown -> CloseWindow -> MouseUp' chain doesn't lead to someone else seeing the MouseUp.
|
|
owner_data.OwnerNext = ImGuiKeyOwner_NoOwner
|
|
end
|
|
owner_data.LockThisFrame = owner_data.LockUntilRelease and key_data.Down
|
|
owner_data.LockUntilRelease = owner_data.LockUntilRelease and key_data.Down -- Clear LockUntilRelease when key is not Down anymore
|
|
end
|
|
|
|
-- Update key routing (for e.g. shortcuts)
|
|
ImGui.UpdateKeyRoutingTable(g.KeysRoutingTable)
|
|
end
|
|
|
|
function ImGui.UpdateMouseInputs()
|
|
local g = GImGui
|
|
local io = g.IO
|
|
|
|
-- Mouse Wheel swapping flag
|
|
-- As a standard behavior holding Shift while using Vertical Mouse Wheel triggers Horizontal scroll instead
|
|
-- - We avoid doing it on OSX as it the OS input layer handles this already.
|
|
-- - FIXME: However this means when running on OSX over Emscripten, Shift+WheelY will incur two swapping (1 in OS, 1 here), canceling the feature.
|
|
-- - FIXME: When we can distinguish e.g. touchpad scroll events from mouse ones, we'll set this accordingly based on input source.
|
|
io.MouseWheelRequestAxisSwap = io.KeyShift and not io.ConfigMacOSXBehaviors
|
|
|
|
-- Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well)
|
|
if (ImGui.IsMousePosValid(io.MousePos)) then
|
|
local x_val = ImFloor(io.MousePos.x)
|
|
local y_val = ImFloor(io.MousePos.y)
|
|
|
|
io.MousePos = ImVec2(x_val, y_val)
|
|
g.MouseLastValidPos = ImVec2(x_val, y_val)
|
|
end
|
|
|
|
-- If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta
|
|
if (ImGui.IsMousePosValid(io.MousePos) and ImGui.IsMousePosValid(io.MousePosPrev)) then
|
|
io.MouseDelta = io.MousePos - io.MousePosPrev
|
|
else
|
|
io.MouseDelta = ImVec2(0.0, 0.0)
|
|
end
|
|
|
|
-- Update stationary timer.
|
|
-- FIXME: May need to rework again to have some tolerance for occasional small movement, while being functional on high-framerates.
|
|
local mouse_stationary_threshold = (io.MouseSource == ImGuiMouseSource.Mouse) and 2.0 or 3.0 -- Slightly higher threshold for ImGuiMouseSource.TouchScreen/ImGuiMouseSource.Pen, may need rework.
|
|
local mouse_stationary = (ImLengthSqr(io.MouseDelta) <= mouse_stationary_threshold * mouse_stationary_threshold)
|
|
g.MouseStationaryTimer = mouse_stationary and (g.MouseStationaryTimer + io.DeltaTime) or 0.0
|
|
-- IMGUI_DEBUG_LOG("%.4f\n", g.MouseStationaryTimer)
|
|
|
|
-- If mouse moved we re-enable mouse hovering in case it was disabled by keyboard/gamepad. In theory should use a >0.0f threshold but would need to reset in everywhere we set this to true.
|
|
if io.MouseDelta.x ~= 0.0 or io.MouseDelta.y ~= 0.0 then
|
|
g.NavHighlightItemUnderNav = false
|
|
end
|
|
|
|
for i = 0, 2 do -- IM_COUNTOF(io.MouseDown)
|
|
io.MouseClicked[i] = io.MouseDown[i] and (io.MouseDownDuration[i] < 0.0)
|
|
io.MouseClickedCount[i] = 0 -- Will be filled below
|
|
io.MouseReleased[i] = not io.MouseDown[i] and (io.MouseDownDuration[i] >= 0.0)
|
|
if (io.MouseReleased[i]) then
|
|
io.MouseReleasedTime[i] = g.Time
|
|
end
|
|
io.MouseDownDurationPrev[i] = io.MouseDownDuration[i]
|
|
if io.MouseDown[i] then
|
|
if io.MouseDownDuration[i] < 0.0 then
|
|
io.MouseDownDuration[i] = 0.0
|
|
else
|
|
io.MouseDownDuration[i] = io.MouseDownDuration[i] + io.DeltaTime
|
|
end
|
|
else
|
|
io.MouseDownDuration[i] = -1.0
|
|
end
|
|
|
|
if io.MouseClicked[i] then
|
|
local is_repeated_click = false
|
|
if (g.Time - io.MouseClickedTime[i]) < io.MouseDoubleClickTime then
|
|
local delta_from_click_pos
|
|
if ImGui.IsMousePosValid(io.MousePos) then
|
|
delta_from_click_pos = io.MousePos - io.MouseClickedPos[i]
|
|
else
|
|
delta_from_click_pos = ImVec2(0.0, 0.0)
|
|
end
|
|
|
|
if ImLengthSqr(delta_from_click_pos) < io.MouseDoubleClickMaxDist * io.MouseDoubleClickMaxDist then
|
|
is_repeated_click = true
|
|
end
|
|
end
|
|
|
|
if is_repeated_click then
|
|
io.MouseClickedLastCount[i] = io.MouseClickedLastCount[i] + 1
|
|
else
|
|
io.MouseClickedLastCount[i] = 1
|
|
end
|
|
|
|
io.MouseClickedTime[i] = g.Time
|
|
io.MouseClickedPos[i] = io.MousePos
|
|
io.MouseClickedCount[i] = io.MouseClickedLastCount[i]
|
|
ImVec2_Copy(io.MouseDragMaxDistanceAbs[i], ImVec2(0.0, 0.0))
|
|
io.MouseDragMaxDistanceSqr[i] = 0.0
|
|
elseif io.MouseDown[i] then
|
|
-- Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold
|
|
local delta_from_click_pos
|
|
if ImGui.IsMousePosValid(io.MousePos) then
|
|
delta_from_click_pos = (io.MousePos - io.MouseClickedPos[i])
|
|
else
|
|
delta_from_click_pos = ImVec2(0.0, 0.0)
|
|
end
|
|
io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], ImLengthSqr(delta_from_click_pos))
|
|
io.MouseDragMaxDistanceAbs[i].x = ImMax(io.MouseDragMaxDistanceAbs[i].x, (delta_from_click_pos.x < 0.0) and -delta_from_click_pos.x or delta_from_click_pos.x)
|
|
io.MouseDragMaxDistanceAbs[i].y = ImMax(io.MouseDragMaxDistanceAbs[i].y, (delta_from_click_pos.y < 0.0) and -delta_from_click_pos.y or delta_from_click_pos.y)
|
|
end
|
|
|
|
-- We provide io.MouseDoubleClicked[] as a legacy service
|
|
io.MouseDoubleClicked[i] = (io.MouseClickedCount[i] == 2)
|
|
|
|
-- Clicking any mouse button reactivate mouse hovering which may have been deactivated by keyboard/gamepad navigation
|
|
if (io.MouseClicked[i]) then
|
|
g.NavHighlightItemUnderNav = false
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param window? ImGuiWindow
|
|
--- @param wheel_amount float
|
|
local function LockWheelingWindow(window, wheel_amount)
|
|
local g = GImGui
|
|
if window then
|
|
g.WheelingWindowReleaseTimer = ImMin(g.WheelingWindowReleaseTimer + ImAbs(wheel_amount) * WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER, WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER)
|
|
else
|
|
g.WheelingWindowReleaseTimer = 0.0
|
|
end
|
|
if (g.WheelingWindow == window) then
|
|
return
|
|
end
|
|
-- IMGUI_DEBUG_LOG_IO("[io] LockWheelingWindow() \"%s\"\n", window ? window->Name : "NULL")
|
|
g.WheelingWindow = window
|
|
ImVec2_Copy(g.WheelingWindowRefMousePos, g.IO.MousePos)
|
|
if window == nil then
|
|
g.WheelingWindowStartFrame = -1
|
|
g.WheelingAxisAvg = ImVec2(0.0, 0.0)
|
|
end
|
|
end
|
|
|
|
--- @param wheel ImVec2
|
|
--- @return ImGuiWindow?
|
|
local function FindBestWheelingWindow(wheel)
|
|
-- For each axis, find window in the hierarchy that may want to use scrolling
|
|
local g = GImGui
|
|
local windows = {nil, nil}
|
|
for axis = ImGuiAxis.X, ImGuiAxis.Y do
|
|
if wheel[axis] ~= 0.0 then
|
|
windows[axis] = g.HoveredWindow
|
|
local window = g.HoveredWindow
|
|
while bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 do
|
|
-- Bubble up into parent window if:
|
|
-- - a child window doesn't allow any scrolling.
|
|
-- - a child window has the ImGuiWindowFlags.NoScrollWithMouse flag.
|
|
-- -- - a child window doesn't need scrolling because it is already at the edge for the direction we are going in (FIXME-WIP)
|
|
local has_scrolling = (window.ScrollMax[axis] ~= 0.0)
|
|
local inputs_disabled = (bit32.band(window.Flags, ImGuiWindowFlags.NoScrollWithMouse) ~= 0) and not (bit32.band(window.Flags, ImGuiWindowFlags.NoMouseInputs) ~= 0)
|
|
-- local scrolling_past_limits = (wheel_v < 0.0) and (window.Scroll[axis] <= 0.0) or (window.Scroll[axis] >= window.ScrollMax[axis])
|
|
|
|
if has_scrolling and not inputs_disabled then -- and not scrolling_past_limits
|
|
break -- select this window
|
|
end
|
|
|
|
windows[axis] = window.ParentWindow
|
|
window = window.ParentWindow
|
|
end
|
|
end
|
|
end
|
|
if windows[1] == nil and windows[2] == nil then
|
|
return nil
|
|
end
|
|
|
|
-- If there's only one window or only one axis then there's no ambiguity
|
|
if (windows[1] == windows[2] or windows[1] == nil or windows[2] == nil) then
|
|
return windows[2] and windows[2] or windows[1]
|
|
end
|
|
|
|
-- If candidate are different windows we need to decide which one to prioritize
|
|
-- - First frame: only find a winner if one axis is zero.
|
|
-- - Subsequent frames: only find a winner when one is more than the other.
|
|
if (g.WheelingWindowStartFrame == -1) then
|
|
g.WheelingWindowStartFrame = g.FrameCount
|
|
end
|
|
if ((g.WheelingWindowStartFrame == g.FrameCount and wheel.x ~= 0.0 and wheel.y ~= 0.0) or (g.WheelingAxisAvg.x == g.WheelingAxisAvg.y)) then
|
|
ImVec2_Copy(g.WheelingWindowWheelRemainder, wheel)
|
|
|
|
return nil
|
|
end
|
|
return (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) and windows[1] or windows[2]
|
|
end
|
|
|
|
-- Called by NewFrame()
|
|
function ImGui.UpdateMouseWheel()
|
|
-- Reset the locked window if we move the mouse or after the timer elapses.
|
|
-- FIXME: Ideally we could refactor to have one timer for "changing window w/ same axis" and a shorter timer for "changing window or axis w/ other axis" (#3795)
|
|
local g = GImGui
|
|
if g.WheelingWindow ~= nil then
|
|
g.WheelingWindowReleaseTimer = g.WheelingWindowReleaseTimer - g.IO.DeltaTime
|
|
if ImGui.IsMousePosValid() and ImLengthSqr(g.IO.MousePos - g.WheelingWindowRefMousePos) > g.IO.MouseDragThreshold * g.IO.MouseDragThreshold then
|
|
g.WheelingWindowReleaseTimer = 0.0
|
|
end
|
|
|
|
if g.WheelingWindowReleaseTimer <= 0.0 then
|
|
LockWheelingWindow(nil, 0.0)
|
|
end
|
|
end
|
|
|
|
local mouse_window = g.WheelingWindow and g.WheelingWindow or g.HoveredWindow
|
|
if not mouse_window or mouse_window.Collapsed then
|
|
return
|
|
end
|
|
|
|
local owner_id = mouse_window.ID
|
|
local wheel = ImVec2()
|
|
wheel.x = ImGui.TestKeyOwner(ImGuiKey.MouseWheelX, owner_id) and g.IO.MouseWheelH or 0.0
|
|
wheel.y = ImGui.TestKeyOwner(ImGuiKey.MouseWheelY, owner_id) and g.IO.MouseWheel or 0.0
|
|
-- IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y)
|
|
if g.WheelingWindow ~= nil then
|
|
ImGui.SetKeyOwner(ImGuiKey.MouseWheelX, owner_id)
|
|
ImGui.SetKeyOwner(ImGuiKey.MouseWheelY, owner_id)
|
|
end
|
|
|
|
-- Zoom / Scale window
|
|
-- FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned.
|
|
if wheel.y ~= 0.0 and g.IO.KeyCtrl and g.IO.FontAllowUserScaling then
|
|
LockWheelingWindow(mouse_window, wheel.y)
|
|
local window = mouse_window
|
|
local new_font_scale = ImClamp(window.FontWindowScale + g.IO.MouseWheel * 0.10, 0.50, 2.50)
|
|
local scale = new_font_scale / window.FontWindowScale
|
|
window.FontWindowScale = new_font_scale
|
|
if window == window.RootWindow then
|
|
local offset = window.Size * (1.0 - scale) * (g.IO.MousePos - window.Pos) / window.Size
|
|
ImGui.SetWindowPos(window, window.Pos + offset, 0)
|
|
-- MarkIniSettingsDirty(window)
|
|
end
|
|
return
|
|
end
|
|
if g.IO.KeyCtrl then
|
|
return
|
|
end
|
|
|
|
-- Mouse wheel scrolling
|
|
-- Read about io.MouseWheelRequestAxisSwap and its issue on Mac+Emscripten in UpdateMouseInputs()
|
|
if (g.IO.MouseWheelRequestAxisSwap) then
|
|
wheel = ImVec2(wheel.y, 0.0)
|
|
end
|
|
|
|
-- Maintain a rough average of moving magnitude on both axes
|
|
-- FIXME: should by based on wall clock time rather than frame-counter
|
|
g.WheelingAxisAvg.x = ImStd.ImExponentialMovingAverage(g.WheelingAxisAvg.x, ImAbs(wheel.x), 30)
|
|
g.WheelingAxisAvg.y = ImStd.ImExponentialMovingAverage(g.WheelingAxisAvg.y, ImAbs(wheel.y), 30)
|
|
|
|
-- In the rare situation where FindBestWheelingWindow() had to defer first frame of wheeling due to ambiguous main axis, reinject it now.
|
|
wheel = wheel + g.WheelingWindowWheelRemainder
|
|
g.WheelingWindowWheelRemainder = ImVec2(0.0, 0.0)
|
|
if (wheel.x == 0.0 and wheel.y == 0.0) then
|
|
return
|
|
end
|
|
|
|
-- Mouse wheel scrolling: find target and apply
|
|
-- - don't renew lock if axis doesn't apply on the window.
|
|
-- - select a main axis when both axes are being moved.
|
|
local window
|
|
if g.WheelingWindow then
|
|
window = g.WheelingWindow
|
|
else
|
|
window = FindBestWheelingWindow(wheel)
|
|
end
|
|
if window then
|
|
if not (bit32.band(window.Flags, ImGuiWindowFlags.NoScrollWithMouse) ~= 0) and not (bit32.band(window.Flags, ImGuiWindowFlags.NoMouseInputs) ~= 0) then
|
|
local do_scroll = { wheel.x ~= 0.0 and window.ScrollMax.x ~= 0.0, wheel.y ~= 0.0 and window.ScrollMax.y ~= 0.0 }
|
|
|
|
if do_scroll[ImGuiAxis.X] and do_scroll[ImGuiAxis.Y] then
|
|
do_scroll[(g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) and ImGuiAxis.Y or ImGuiAxis.X] = false
|
|
end
|
|
|
|
if do_scroll[ImGuiAxis.X] then
|
|
LockWheelingWindow(window, wheel.x)
|
|
local max_step = window.InnerRect:GetWidth() * 0.67
|
|
local scroll_step = ImTrunc(ImMin(2 * window.FontRefSize, max_step))
|
|
ImGui.SetScrollX(window, window.Scroll.x - wheel.x * scroll_step)
|
|
g.WheelingWindowScrolledFrame = g.FrameCount
|
|
end
|
|
|
|
if do_scroll[ImGuiAxis.Y] then
|
|
LockWheelingWindow(window, wheel.y)
|
|
local max_step = window.InnerRect:GetHeight() * 0.67
|
|
local scroll_step = ImTrunc(ImMin(5 * window.FontRefSize, max_step))
|
|
ImGui.SetScrollY(window, window.Scroll.y - wheel.y * scroll_step)
|
|
g.WheelingWindowScrolledFrame = g.FrameCount
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Called once a frame. Followed by SetCurrentFont() which sets up the remaining data.
|
|
-- FIXME-VIEWPORT: the concept of a single ClipRectFullscreen is not ideal!
|
|
local function SetupDrawListSharedData()
|
|
local g = GImGui
|
|
local virtual_space = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX)
|
|
for _, viewport in g.Viewports:iter() do
|
|
virtual_space:Add(viewport:GetMainRect())
|
|
end
|
|
g.DrawListSharedData.ClipRectFullscreen = virtual_space:ToVec4()
|
|
g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol
|
|
g.DrawListSharedData:SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError)
|
|
g.DrawListSharedData.InitialFlags = ImDrawListFlags.None
|
|
if g.Style.AntiAliasedLines then
|
|
g.DrawListSharedData.InitialFlags = bit32.bor(g.DrawListSharedData.InitialFlags, ImDrawListFlags.AntiAliasedLines)
|
|
end
|
|
if g.Style.AntiAliasedLinesUseTex and not bit32.band(g.IO.Fonts.Flags, ImFontAtlasFlags.NoBakedLines) then
|
|
g.DrawListSharedData.InitialFlags = bit32.bor(g.DrawListSharedData.InitialFlags, ImDrawListFlags.AntiAliasedLinesUseTex)
|
|
end
|
|
if g.Style.AntiAliasedFill then
|
|
g.DrawListSharedData.InitialFlags = bit32.bor(g.DrawListSharedData.InitialFlags, ImDrawListFlags.AntiAliasedFill)
|
|
end
|
|
if bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.RendererHasVtxOffset) then
|
|
g.DrawListSharedData.InitialFlags = bit32.bor(g.DrawListSharedData.InitialFlags, ImDrawListFlags.AllowVtxOffset)
|
|
end
|
|
g.DrawListSharedData.InitialFringeScale = 1.0
|
|
end
|
|
|
|
--- @param viewport ImGuiViewportP
|
|
local function InitViewportDrawData(viewport)
|
|
local g = GImGui
|
|
local draw_data = viewport.DrawDataP
|
|
|
|
viewport.DrawData = draw_data -- Make publicly accessible
|
|
viewport.DrawDataBuilder.Layers[1] = draw_data.CmdLists
|
|
viewport.DrawDataBuilder.Layers[2] = viewport.DrawDataBuilder.LayerData1
|
|
viewport.DrawDataBuilder.Layers[1]:resize(0)
|
|
viewport.DrawDataBuilder.Layers[2]:resize(0)
|
|
|
|
-- When minimized, we report draw_data->DisplaySize as zero to be consistent with non-viewport mode,
|
|
-- and to allow applications/backends to easily skip rendering.
|
|
-- FIXME: Note that we however do NOT attempt to report "zero drawlist / vertices" into the ImDrawData structure.
|
|
-- This is because the work has been done already, and its wasted! We should fix that and add optimizations for
|
|
-- it earlier in the pipeline, rather than pretend to hide the data at the end of the pipeline.
|
|
local is_minimized = bit32.band(viewport.Flags, ImGuiViewportFlags.IsMinimized) ~= 0
|
|
|
|
draw_data.Valid = true
|
|
draw_data.FrameCount = g.FrameCount
|
|
draw_data.TotalVtxCount = 0
|
|
draw_data.TotalIdxCount = 0
|
|
draw_data.DisplayPos = viewport.Pos
|
|
if is_minimized then
|
|
draw_data.DisplaySize = ImVec2(0.0, 0.0)
|
|
else
|
|
ImVec2_Copy(draw_data.DisplaySize, viewport.Size)
|
|
end
|
|
if viewport.FramebufferScale.x ~= 0.0 then
|
|
draw_data.FramebufferScale = viewport.FramebufferScale
|
|
else
|
|
draw_data.FramebufferScale = g.IO.DisplayFramebufferScale
|
|
end
|
|
|
|
draw_data.OwnerViewport = viewport
|
|
draw_data.Textures = g.PlatformIO.Textures
|
|
end
|
|
|
|
-- FIXME-DPI: PLEASE DO NOT USE. I don't want to formalize this just yet. Because reasons.
|
|
function ImGui.GetScale()
|
|
local g = GImGui
|
|
return g.Style._MainScale
|
|
end
|
|
|
|
--- @return ImGuiWindow
|
|
function ImGui.GetCurrentWindowRead()
|
|
local g = GImGui
|
|
return g.CurrentWindow
|
|
end
|
|
|
|
--- @return ImGuiWindow
|
|
@native
|
|
function ImGui.GetCurrentWindow()
|
|
local g = GImGui
|
|
g.CurrentWindow.WriteAccessed = true
|
|
return g.CurrentWindow
|
|
end
|
|
|
|
-- Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering.
|
|
-- - When using this function it is sane to ensure that float are perfectly rounded to integer values,
|
|
-- so that e.g. (int)(max.x-min.x) in user's render produce correct result.
|
|
-- - If the code here changes, may need to update code of functions like NextColumn() and PushColumnClipRect():
|
|
-- some frequently called functions which to modify both channels and clipping simultaneously tend to use the
|
|
-- more specialized SetWindowClipRectBeforeSetChannel() to avoid extraneous updates of underlying ImDrawCmds.
|
|
-- - This is analogous to PushFont()/PopFont() in the sense that are a mixing a global stack and a window stack,
|
|
-- which in the case of ClipRect is not so problematic but tends to be more restrictive for fonts.
|
|
--- @param clip_rect_min ImVec2
|
|
--- @param clip_rect_max ImVec2
|
|
--- @param intersect_with_current_clip_rect? bool
|
|
@native
|
|
function ImGui.PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect)
|
|
local window = ImGui.GetCurrentWindow()
|
|
window.DrawList:PushClipRect(clip_rect_min, clip_rect_max, intersect_with_current_clip_rect)
|
|
ImRect_CopyFromV4(window.ClipRect, window.DrawList._ClipRectStack:back())
|
|
end
|
|
|
|
function ImGui.PopClipRect()
|
|
local window = ImGui.GetCurrentWindow()
|
|
window.DrawList:PopClipRect()
|
|
ImRect_CopyFromV4(window.ClipRect, window.DrawList._ClipRectStack:back())
|
|
end
|
|
|
|
--- @param viewport ImGuiViewportP
|
|
--- @param drawlist_no size_t # background(1), foreground(2)
|
|
--- @param drawlist_name string
|
|
--- @return ImDrawList
|
|
local function GetViewportBgFgDrawList(viewport, drawlist_no, drawlist_name)
|
|
-- Create the draw list on demand, because they are not frequently used for all viewports
|
|
local g = GImGui
|
|
IM_ASSERT(drawlist_no <= 2) -- IM_COUNTOF(viewport->BgFgDrawLists)
|
|
local draw_list = viewport.BgFgDrawLists[drawlist_no]
|
|
if draw_list == nil then
|
|
draw_list = ImDrawList(g.DrawListSharedData)
|
|
draw_list._OwnerName = drawlist_name
|
|
viewport.BgFgDrawLists[drawlist_no] = draw_list
|
|
end
|
|
|
|
-- Our ImDrawList system requires that there is always a command
|
|
if viewport.BgFgDrawListsLastTimeActive[drawlist_no] ~= g.Time then
|
|
draw_list:_ResetForNewFrame()
|
|
draw_list:PushTexture(g.IO.Fonts.TexRef)
|
|
draw_list:PushClipRect(viewport.Pos, viewport.Pos + viewport.Size, false)
|
|
viewport.BgFgDrawListsLastTimeActive[drawlist_no] = g.Time
|
|
end
|
|
|
|
return draw_list
|
|
end
|
|
|
|
--- @param viewport? ImGuiViewport
|
|
function ImGui.GetBackgroundDrawList(viewport)
|
|
local g = GImGui
|
|
|
|
if (viewport == nil) then
|
|
viewport = g.CurrentWindow.Viewport
|
|
end
|
|
|
|
--- @cast viewport ImGuiViewportP
|
|
return GetViewportBgFgDrawList(viewport, 1, "##Background")
|
|
end
|
|
|
|
--- @param viewport? ImGuiViewport
|
|
function ImGui.GetForegroundDrawList(viewport)
|
|
local g = GImGui
|
|
|
|
if (viewport == nil) then
|
|
viewport = g.CurrentWindow.Viewport
|
|
end
|
|
|
|
--- @cast viewport ImGuiViewportP
|
|
return GetViewportBgFgDrawList(viewport, 2, "##Foreground")
|
|
end
|
|
|
|
-- FIXME: Add a more explicit sort order in the window structure.
|
|
--- @param lhs ImGuiWindow
|
|
--- @param rhs ImGuiWindow
|
|
local function ChildWindowComparer(lhs, rhs)
|
|
local a, b = lhs, rhs
|
|
local d = (bit32.band(a.Flags, ImGuiWindowFlags.Popup) ~= 0 and 1 or 0) - (bit32.band(b.Flags, ImGuiWindowFlags.Popup) ~= 0 and 1 or 0)
|
|
if d ~= 0 then
|
|
return (d < 0)
|
|
end
|
|
d = (bit32.band(a.Flags, ImGuiWindowFlags.Tooltip) ~= 0 and 1 or 0) - (bit32.band(b.Flags, ImGuiWindowFlags.Tooltip) ~= 0 and 1 or 0)
|
|
if d ~= 0 then
|
|
return (d < 0)
|
|
end
|
|
return (a.BeginOrderWithinParent - b.BeginOrderWithinParent < 0)
|
|
end
|
|
|
|
--- @param out_sorted_windows ImVector<ImGuiWindow>
|
|
--- @param window ImGuiWindow
|
|
local function AddWindowToSortBuffer(out_sorted_windows, window)
|
|
out_sorted_windows:push_back(window)
|
|
if window.Active then
|
|
local count = window.DC.ChildWindows.Size
|
|
ImStd.ImQsort(window.DC.ChildWindows.Data, count, ChildWindowComparer)
|
|
for i = 1, count do
|
|
local child = window.DC.ChildWindows[i]
|
|
if child.Active then
|
|
AddWindowToSortBuffer(out_sorted_windows, child)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function AddWindowToDrawData(window, layer)
|
|
local g = GImGui
|
|
local viewport = window.Viewport
|
|
g.IO.MetricsRenderWindows = g.IO.MetricsRenderWindows + 1
|
|
if window.DrawList._Splitter._Count > 1 then
|
|
window.DrawList:ChannelsMerge() -- Merge if user forgot to merge back. Also required in Docking branch for ImGuiWindowFlags.DockNodeHost windows.
|
|
end
|
|
ImGui.AddDrawListToDrawDataEx(viewport.DrawDataP, viewport.DrawDataBuilder.Layers[layer], window.DrawList)
|
|
for _, child in window.DC.ChildWindows:iter() do
|
|
if (ImGui.IsWindowActiveAndVisible(child)) then -- Clipped children may have been marked not active
|
|
AddWindowToDrawData(child, layer)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
local function GetWindowDisplayLayer(window)
|
|
return (bit32.band(window.Flags, ImGuiWindowFlags.Tooltip) ~= 0) and 2 or 1
|
|
end
|
|
|
|
-- Layer is locked for the root window, however child windows may use a different viewport (e.g. extruding menu)
|
|
--- @param window ImGuiWindow
|
|
local function AddRootWindowToDrawData(window)
|
|
AddWindowToDrawData(window, GetWindowDisplayLayer(window))
|
|
end
|
|
|
|
--- @param builder ImDrawDataBuilder
|
|
local function FlattenDrawDataIntoSingleLayer(builder)
|
|
local n = builder.Layers[1].Size
|
|
local full_size = n
|
|
|
|
for i = 2, #builder.Layers do
|
|
full_size = full_size + builder.Layers[i].Size
|
|
end
|
|
|
|
builder.Layers[1]:resize(full_size)
|
|
|
|
for layer_n = 2, #builder.Layers do
|
|
local layer = builder.Layers[layer_n]
|
|
if not layer:empty() then
|
|
for i = 1, layer.Size do
|
|
builder.Layers[1].Data[n + i] = layer.Data[i]
|
|
end
|
|
|
|
n = n + layer.Size
|
|
|
|
layer:resize(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.NewFrame()
|
|
IM_ASSERT(GImGui ~= nil, "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?")
|
|
local g = GImGui
|
|
|
|
g.ConfigFlagsLastFrame = g.ConfigFlagsCurrFrame
|
|
g.ConfigFlagsCurrFrame = g.IO.ConfigFlags
|
|
|
|
-- Load settings on first frame, save settings when modified (after a delay)
|
|
ImGui.UpdateSettings()
|
|
|
|
g.Time = g.Time + g.IO.DeltaTime
|
|
|
|
g.FrameCount = g.FrameCount + 1
|
|
g.TooltipOverrideCount = 0
|
|
g.WindowsActiveCount = 0
|
|
g.MenusIdSubmittedThisFrame:resize(0)
|
|
|
|
-- Calculate frame-rate for the user, as a purely luxurious feature
|
|
-- FIXME: are lines below correct and necessary
|
|
g.FramerateSecPerFrameAccum = g.FramerateSecPerFrameAccum + (g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx])
|
|
g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime
|
|
g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % 60
|
|
g.FramerateSecPerFrameCount = ImMin(g.FramerateSecPerFrameCount + 1, 60)
|
|
if g.FramerateSecPerFrameAccum > 0 then
|
|
g.IO.Framerate = (1.0 / (g.FramerateSecPerFrameAccum / g.FramerateSecPerFrameCount))
|
|
else
|
|
g.IO.Framerate = FLT_MAX
|
|
end
|
|
|
|
-- Process input queue (trickle as many events as possible), turn events into writes to IO structure
|
|
ImGui.UpdateInputEvents(g.IO.ConfigInputTrickleEventQueue)
|
|
|
|
-- Update viewports (after processing input queue, so io.MouseHoveredViewport is set)
|
|
ImGui.UpdateViewportsNewFrame()
|
|
|
|
-- Update texture list (collect destroyed textures, etc.)
|
|
ImGui.UpdateTexturesNewFrame()
|
|
|
|
-- Setup current font and draw list shared data
|
|
SetupDrawListSharedData()
|
|
ImGui.UpdateFontsNewFrame()
|
|
|
|
g.WithinFrameScope = true
|
|
|
|
-- Mark rendering data as invalid to prevent user who may have a handle on it to use it.
|
|
for _, viewport in g.Viewports:iter() do
|
|
viewport.DrawData = nil
|
|
viewport.DrawDataP.Valid = false
|
|
end
|
|
|
|
-- Drag and drop keep the source ID alive so even if the source disappear our state is consistent
|
|
if g.DragDropActive and g.DragDropPayload.SourceId == g.ActiveId then
|
|
ImGui.KeepAliveID(g.DragDropPayload.SourceId)
|
|
end
|
|
|
|
-- [DEBUG]
|
|
if not g.IO.ConfigDebugHighlightIdConflicts or not g.IO.KeyCtrl then -- Count is locked while holding Ctrl
|
|
g.DebugDrawIdConflictsId = 0
|
|
end
|
|
if g.IO.ConfigDebugHighlightIdConflicts and g.HoveredIdPreviousFrameItemCount > 1 then
|
|
g.DebugDrawIdConflictsId = g.HoveredIdPreviousFrame
|
|
end
|
|
|
|
-- Update HoveredId data
|
|
if not g.HoveredIdPreviousFrame then
|
|
g.HoveredIdTimer = 0.0
|
|
end
|
|
if not g.HoveredIdPreviousFrame or (g.HoveredId and g.ActiveId == g.HoveredId) then
|
|
g.HoveredIdNotActiveTimer = 0.0
|
|
end
|
|
if g.HoveredId then
|
|
g.HoveredIdTimer = g.HoveredIdTimer + g.IO.DeltaTime
|
|
end
|
|
if g.HoveredId and g.ActiveId ~= g.HoveredId then
|
|
g.HoveredIdNotActiveTimer = g.HoveredIdNotActiveTimer + g.IO.DeltaTime
|
|
end
|
|
g.HoveredIdPreviousFrame = g.HoveredId
|
|
g.HoveredIdPreviousFrameItemCount = 0
|
|
g.HoveredId = 0
|
|
g.HoveredIdAllowOverlap = false
|
|
g.HoveredIdIsDisabled = false
|
|
|
|
-- Clear ActiveID if the item is not alive anymore.
|
|
-- In 1.87, the common most call to KeepAliveID() was moved from GetID() to ItemAdd().
|
|
-- As a result, custom widget using ButtonBehavior() _without_ ItemAdd() need to call KeepAliveID() themselves.
|
|
if (g.ActiveId ~= 0 and g.ActiveIdIsAlive ~= g.ActiveId and g.ActiveIdPreviousFrame == g.ActiveId) then
|
|
IMGUI_DEBUG_LOG_ACTIVEID("NewFrame(): ClearActiveID() 0x%08X because it isn't marked alive anymore!", g.ActiveId)
|
|
|
|
ImGui.ClearActiveID()
|
|
end
|
|
|
|
-- Update ActiveId data (clear reference to active widget if the widget isn't alive anymore)
|
|
if g.ActiveId then
|
|
g.ActiveIdTimer = g.ActiveIdTimer + g.IO.DeltaTime
|
|
end
|
|
if g.ActiveId and g.ActiveId == g.LastActiveId then
|
|
g.LastActiveIdWasSelected = g.ActiveIdWasSelected
|
|
g.LastActiveIdWasSoleSelected = g.ActiveIdWasSoleSelected
|
|
end
|
|
|
|
g.LastActiveIdTimer = g.LastActiveIdTimer + g.IO.DeltaTime
|
|
g.ActiveIdPreviousFrame = g.ActiveId
|
|
g.ActiveIdIsAlive = 0
|
|
g.AnyIdHasBeenEditedThisFrame = false
|
|
g.ActiveIdHasBeenEditedThisFrame = false
|
|
g.ActiveIdIsJustActivated = false
|
|
if g.TempInputId ~= 0 and g.ActiveId ~= g.TempInputId then
|
|
g.TempInputId = 0
|
|
end
|
|
if g.InputTextReactivateId ~= 0 and g.InputTextReactivateId ~= g.DeactivatedItemData.ID then
|
|
g.InputTextReactivateId = 0
|
|
end
|
|
if g.ActiveId == 0 then
|
|
g.ActiveIdUsingNavDirMask = 0x00
|
|
g.ActiveIdUsingAllKeyboardKeys = false
|
|
end
|
|
if g.DeactivatedItemData.ElapseFrame < g.FrameCount then
|
|
g.DeactivatedItemData.ID = 0
|
|
end
|
|
g.DeactivatedItemData.IsAlive = false
|
|
if g.InputTextDeactivatedState.ElapseFrame < g.FrameCount then
|
|
g.InputTextDeactivatedState.ID = 0
|
|
end
|
|
|
|
-- Record when we have been stationary as this state is preserved while over same item.
|
|
-- FIXME: The way this is expressed means user cannot alter HoverStationaryDelay during the frame to use varying values.
|
|
-- To allow this we should store HoverItemMaxStationaryTime+ID and perform the >= check in IsItemHovered() function.
|
|
if g.HoverItemDelayId ~= 0 and g.MouseStationaryTimer >= g.Style.HoverStationaryDelay then
|
|
g.HoverItemUnlockedStationaryId = g.HoverItemDelayId
|
|
elseif g.HoverItemDelayId == 0 then
|
|
g.HoverItemUnlockedStationaryId = 0
|
|
end
|
|
if g.HoveredWindow ~= nil and g.MouseStationaryTimer >= g.Style.HoverStationaryDelay then
|
|
g.HoverWindowUnlockedStationaryId = g.HoveredWindow.ID
|
|
elseif g.HoveredWindow == nil then
|
|
g.HoverWindowUnlockedStationaryId = 0
|
|
end
|
|
|
|
-- Update hover delay for IsItemHovered() with delays and tooltips
|
|
g.HoverItemDelayIdPreviousFrame = g.HoverItemDelayId
|
|
if g.HoverItemDelayId ~= 0 then
|
|
g.HoverItemDelayTimer = g.HoverItemDelayTimer + g.IO.DeltaTime
|
|
g.HoverItemDelayClearTimer = 0.0
|
|
g.HoverItemDelayId = 0
|
|
elseif g.HoverItemDelayTimer > 0.0 then
|
|
-- This gives a little bit of leeway before clearing the hover timer, allowing mouse to cross gaps
|
|
-- We could expose 0.25f as style.HoverClearDelay but I am not sure of the logic yet, this is particularly subtle.
|
|
g.HoverItemDelayClearTimer = g.HoverItemDelayClearTimer + g.IO.DeltaTime
|
|
if g.HoverItemDelayClearTimer >= ImMax(0.25, g.IO.DeltaTime * 2.0) then -- ~7 frames at 30 Hz + allow for low framerate
|
|
g.HoverItemDelayTimer = 0.0
|
|
g.HoverItemDelayClearTimer = 0.0 -- May want a decaying timer, in which case need to clamp at max first, based on max of caller last requested timer.
|
|
end
|
|
end
|
|
|
|
-- Close popups on focus lost (currently wip/opt-in)
|
|
-- if (g.IO.AppFocusLost)
|
|
-- ClosePopupsExceptModals();
|
|
|
|
-- Update keyboard input state
|
|
ImGui.UpdateKeyboardInputs()
|
|
|
|
-- IM_ASSERT(g.IO.KeyCtrl == IsKeyDown(ImGuiKey_LeftCtrl) || IsKeyDown(ImGuiKey_RightCtrl));
|
|
-- IM_ASSERT(g.IO.KeyShift == IsKeyDown(ImGuiKey_LeftShift) || IsKeyDown(ImGuiKey_RightShift));
|
|
-- IM_ASSERT(g.IO.KeyAlt == IsKeyDown(ImGuiKey_LeftAlt) || IsKeyDown(ImGuiKey_RightAlt));
|
|
-- IM_ASSERT(g.IO.KeySuper == IsKeyDown(ImGuiKey_LeftSuper) || IsKeyDown(ImGuiKey_RightSuper));
|
|
|
|
-- Drag and drop
|
|
g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr
|
|
g.DragDropAcceptIdCurr = 0
|
|
g.DragDropAcceptFlagsPrev = g.DragDropAcceptFlagsCurr
|
|
g.DragDropAcceptFlagsCurr = ImGuiDragDropFlags.None
|
|
g.DragDropAcceptIdCurrRectSurface = FLT_MAX
|
|
g.DragDropWithinSource = false
|
|
g.DragDropWithinTarget = false
|
|
g.DragDropHoldJustPressedId = 0
|
|
if g.DragDropActive then
|
|
-- Also works when g.ActiveId==0 (aka leftover payload in progress, no active id)
|
|
-- You may disable this externally by hijacking the input route:
|
|
-- 'if (GetDragDropPayload() ~= nil) then Shortcut(ImGuiKey.Escape, ImGuiInputFlags.RouteGlobal | ImGuiInputFlags.RouteOverActive) end
|
|
-- but you will not get a return value from Shortcut() due to ActiveIdUsingAllKeyboardKeys logic. You can however poll IsKeyPressed(ImGuiKey.Escape) afterwards.
|
|
local owner_id = (g.ActiveId ~= 0) and g.ActiveId or ImHashStr("##DragDropCancelHandler")
|
|
if ImGui.Shortcut(ImGuiKey.Escape, ImGuiInputFlags.RouteGlobal, owner_id) then
|
|
ImGui.ClearActiveID()
|
|
ImGui.ClearDragDrop()
|
|
end
|
|
end
|
|
g.TooltipPreviousWindow = nil
|
|
|
|
-- Update keyboard/gamepad navigation
|
|
ImGui.NavUpdate()
|
|
|
|
-- Update mouse input state
|
|
ImGui.UpdateMouseInputs()
|
|
|
|
-- Undocking
|
|
-- (needs to be before UpdateMouseMovingWindowNewFrame so the window is already offset and following the mouse on the detaching frame)
|
|
ImGui.DockContextNewFrameUpdateUndocking(g)
|
|
|
|
-- Mark all windows as not visible and compact unused memory.
|
|
IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size)
|
|
local gc_all = g.GcCompactAll or g.IO.ConfigMemoryCompactTimer < 0.0
|
|
local memory_compact_start_time = gc_all and FLT_MAX or (g.Time - g.IO.ConfigMemoryCompactTimer)
|
|
|
|
for _, window in g.Windows:iter() do
|
|
window.WasActive = window.Active
|
|
window.Active = false
|
|
window.WriteAccessed = false
|
|
window.BeginCountPreviousFrame = window.BeginCount
|
|
window.BeginCount = 0
|
|
|
|
-- Garbage collect transient buffers of recently unused windows
|
|
if (not window.WasActive or gc_all) and not window.MemoryCompacted and window.LastTimeActive < memory_compact_start_time then
|
|
ImGui.GcCompactTransientWindowBuffers(window)
|
|
end
|
|
end
|
|
|
|
-- Find hovered window
|
|
-- (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame)
|
|
-- (currently needs to be done after the WasActive=Active loop and FindHoveredWindowEx uses ->Active)
|
|
ImGui.UpdateHoveredWindowAndCaptureFlags(g.IO.MousePos)
|
|
|
|
-- Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering)
|
|
ImGui.UpdateMouseMovingWindowNewFrame()
|
|
|
|
-- Background darkening/whitening
|
|
if ImGui.GetTopMostPopupModal() ~= nil or (g.NavWindowingTarget ~= nil and g.NavWindowingHighlightAlpha > 0.0) then
|
|
g.DimBgRatio = ImMin(g.DimBgRatio + g.IO.DeltaTime * 6.0, 1.0)
|
|
else
|
|
g.DimBgRatio = ImMax(g.DimBgRatio - g.IO.DeltaTime * 10.0, 0.0)
|
|
end
|
|
|
|
g.MouseCursor = ImGuiMouseCursor.Arrow
|
|
g.WantCaptureMouseNextFrame = -1
|
|
g.WantCaptureKeyboardNextFrame = -1
|
|
g.WantTextInputNextFrame = -1
|
|
|
|
-- Platform IME data: reset for the frame
|
|
ImGuiPlatformImeData_Copy(g.PlatformImeDataPrev, g.PlatformImeData)
|
|
g.PlatformImeData.WantVisible = false
|
|
g.PlatformImeData.WantTextInput = false
|
|
|
|
-- Mouse wheel scrolling, scale
|
|
ImGui.UpdateMouseWheel()
|
|
|
|
-- Garbage collect transient buffers of recently unused tables
|
|
for i = 1, g.TablesLastTimeActive.Size do
|
|
if g.TablesLastTimeActive.Data[i] >= 0.0 and g.TablesLastTimeActive.Data[i] < memory_compact_start_time then
|
|
ImGui.TableGcCompactTransientBuffers(g.Tables:GetByIndex(i - 1))
|
|
end
|
|
end
|
|
for _, table_temp_data in g.TablesTempData:iter() do
|
|
table_temp_data.ReconcileColumnsRequests:clear() -- Unusual: clear every frame because this is rarely used.
|
|
if table_temp_data.LastTimeActive >= 0.0 and table_temp_data.LastTimeActive < memory_compact_start_time then
|
|
ImGui.TableGcCompactTransientBuffers(table_temp_data)
|
|
end
|
|
end
|
|
if g.GcCompactAll then
|
|
ImGui.GcCompactTransientMiscBuffers()
|
|
end
|
|
g.GcCompactAll = false
|
|
|
|
-- Closing the focused window restore focus to the first active root window in descending z-order
|
|
if (g.NavWindow and not g.NavWindow.WasActive) then
|
|
ImGui.FocusTopMostWindowUnderOne(nil, nil, nil, ImGuiFocusRequestFlags.RestoreFocusedChild)
|
|
end
|
|
|
|
-- No window should be open at the beginning of the frame.
|
|
-- But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear.
|
|
g.CurrentWindowStack:resize(0)
|
|
g.BeginPopupStack:resize(0)
|
|
g.ItemFlagsStack:resize(0)
|
|
g.ItemFlagsStack:push_back(ImGuiItemFlags.Default_) -- Default flags
|
|
g.CurrentItemFlags = g.ItemFlagsStack:back()
|
|
g.GroupStack:resize(0)
|
|
|
|
-- [DEBUG] ID Stack Tool query
|
|
ImGui.UpdateDebugToolItemPathQuery()
|
|
|
|
-- Docking
|
|
ImGui.DockContextNewFrameUpdateDocking(g)
|
|
|
|
-- Create implicit/fallback window - which we will only render it if the user has added something to it.
|
|
-- We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags.
|
|
-- This fallback is particularly important as it prevents ImGui:: calls from crashing.
|
|
g.WithinFrameScopeWithImplicitWindow = true
|
|
ImGui.SetNextWindowSize(ImVec2(400, 400), ImGuiCond.FirstUseEver)
|
|
ImGui.Begin("Debug##Default")
|
|
IM_ASSERT(g.CurrentWindow.IsFallbackWindow == true)
|
|
|
|
-- Store stack sizes
|
|
g.ErrorCountCurrentFrame = 0
|
|
g.StackSizesInNewFrame = g.StackSizesInNewFrame or ImGuiErrorRecoveryState()
|
|
ImGui.ErrorRecoveryStoreState(g.StackSizesInNewFrame)
|
|
end
|
|
|
|
-- This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal.
|
|
function ImGui.EndFrame()
|
|
local g = GImGui
|
|
IM_ASSERT(g.Initialized)
|
|
|
|
-- Don't process EndFrame() multiple times.
|
|
if g.FrameCountEnded == g.FrameCount then
|
|
return
|
|
end
|
|
IM_ASSERT_USER_ERROR(g.WithinFrameScope, "Forgot to call ImGui::NewFrame()?")
|
|
|
|
-- [EXPERIMENTAL] Recover from errors
|
|
if g.IO.ConfigErrorRecovery and g.StackSizesInNewFrame then
|
|
ImGui.ErrorRecoveryTryToRecoverState(g.StackSizesInNewFrame)
|
|
end
|
|
ImGui.ErrorCheckEndFrameFinalizeErrorTooltip()
|
|
|
|
-- Notify Platform/OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME)
|
|
local ime_data = g.PlatformImeData
|
|
if g.PlatformIO.Platform_SetImeDataFn ~= nil and not ImGuiPlatformImeData_Compare(ime_data, g.PlatformImeDataPrev) then
|
|
local viewport = ImGui.FindViewportByID(ime_data.ViewportId)
|
|
if viewport == nil then
|
|
viewport = ImGui.GetMainViewport()
|
|
end
|
|
-- IMGUI_DEBUG_LOG_IO("[io] Calling Platform_SetImeDataFn(): WantVisible: %d, InputPos (%.2f,%.2f) for Viewport 0x%08X", ime_data.WantVisible, ime_data.InputPos.x, ime_data.InputPos.y, viewport.ID)
|
|
g.PlatformIO.Platform_SetImeDataFn(g, viewport, ime_data)
|
|
end
|
|
g.WantTextInputNextFrame = ime_data.WantTextInput
|
|
|
|
-- Hide implicit/fallback "Debug" window if it hasn't been used
|
|
g.WithinFrameScopeWithImplicitWindow = false
|
|
if (g.CurrentWindow and g.CurrentWindow.IsFallbackWindow and g.CurrentWindow.WriteAccessed == false) then
|
|
g.CurrentWindow.Active = false
|
|
end
|
|
ImGui.End()
|
|
|
|
-- Update navigation: Ctrl+Tab, wrap-around requests
|
|
ImGui.NavEndFrame()
|
|
|
|
-- Update docking
|
|
ImGui.DockContextEndFrame(g)
|
|
|
|
ImGui.SetCurrentViewport(nil, nil)
|
|
|
|
-- Drag and Drop: Elapse payload (if delivered, or if source stops being submitted)
|
|
if g.DragDropActive then
|
|
local is_delivered = g.DragDropPayload.Delivery
|
|
local is_elapsed = (g.DragDropSourceFrameCount + 1 < g.FrameCount) and (bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.PayloadAutoExpire) ~= 0 or g.DragDropMouseButton == -1 or not ImGui.IsMouseDown(g.DragDropMouseButton))
|
|
if is_delivered or is_elapsed then ImGui.ClearDragDrop() end
|
|
end
|
|
|
|
-- Drag and Drop: Fallback for missing source tooltip. This is not ideal but better than nothing.
|
|
-- If you want to handle source item disappearing: instead of submitting your description tooltip
|
|
-- in the BeginDragDropSource() block of the dragged item, you can submit them from a safe single spot
|
|
-- (e.g. end of your item loop, or before EndFrame) by reading payload data.
|
|
-- In the typical case, the contents of drag tooltip should be possible to infer solely from payload data.
|
|
if g.DragDropActive and g.DragDropSourceFrameCount + 1 < g.FrameCount and (bit32.band(g.DragDropSourceFlags, ImGuiDragDropFlags.SourceNoPreviewTooltip) == 0) then
|
|
g.DragDropWithinSource = true
|
|
ImGui.SetTooltip("...")
|
|
g.DragDropWithinSource = false
|
|
end
|
|
|
|
-- End frame
|
|
g.WithinFrameScope = false
|
|
g.FrameCountEnded = g.FrameCount
|
|
ImGui.UpdateFontsEndFrame()
|
|
|
|
-- Initiate moving window + handle left-click and right-click focus
|
|
ImGui.UpdateMouseMovingWindowEndFrame()
|
|
|
|
-- Update user-facing viewport list (g.Viewports -> g.PlatformIO.Viewports after filtering out some)
|
|
ImGui.UpdateViewportsEndFrame()
|
|
|
|
-- Sort the window list so that all child windows are after their parent
|
|
-- We cannot do that on FocusWindow() because children may not exist yet
|
|
g.WindowsTempSortBuffer:resize(0)
|
|
g.WindowsTempSortBuffer:reserve(g.Windows.Size)
|
|
for _, window in g.Windows:iter() do
|
|
repeat
|
|
if window.Active and bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then -- if a child is active its parent will add it
|
|
break
|
|
end
|
|
|
|
AddWindowToSortBuffer(g.WindowsTempSortBuffer, window)
|
|
|
|
until true
|
|
end
|
|
|
|
-- This usually assert if there is a mismatch between the ImGuiWindowFlags.ChildWindow / ParentWindow values and DC.ChildWindows[] in parents, aka we've done something wrong.
|
|
IM_ASSERT(g.Windows.Size == g.WindowsTempSortBuffer.Size)
|
|
g.Windows:swap(g.WindowsTempSortBuffer)
|
|
g.IO.MetricsActiveWindows = g.WindowsActiveCount
|
|
|
|
ImGui.UpdateTexturesEndFrame()
|
|
|
|
-- Unlock font atlas
|
|
for _, atlas in g.FontAtlases:iter() do
|
|
atlas.Locked = false
|
|
end
|
|
|
|
-- Clear Input data for next frame
|
|
ImVec2_Copy(g.IO.MousePosPrev, g.IO.MousePos)
|
|
g.IO.AppFocusLost = false
|
|
g.IO.MouseWheel = 0.0
|
|
g.IO.MouseWheelH = 0.0
|
|
g.IO.InputQueueCharacters:resize(0)
|
|
end
|
|
|
|
-- TODO:
|
|
-- Prepare the data for rendering so you can call GetDrawData()
|
|
-- (As with anything within the ImGui:: namespace this doesn't touch your GPU or graphics API at all:
|
|
-- it is the role of the ImGui_ImplXXXX_RenderDrawData() function provided by the renderer backend)
|
|
function ImGui.Render()
|
|
local g = GImGui
|
|
IM_ASSERT(g.Initialized)
|
|
|
|
if g.FrameCountEnded ~= g.FrameCount then
|
|
ImGui.EndFrame()
|
|
end
|
|
if g.FrameCountRendered == g.FrameCount then return end
|
|
g.FrameCountRendered = g.FrameCount
|
|
|
|
g.IO.MetricsRenderWindows = 0
|
|
|
|
-- Add background ImDrawList (for each active viewport)
|
|
for _, viewport in g.Viewports:iter() do
|
|
InitViewportDrawData(viewport)
|
|
if viewport.BgFgDrawLists[1] ~= nil and viewport.BgFgDrawListsLastTimeActive[1] == g.Time then
|
|
ImGui.AddDrawListToDrawDataEx(viewport.DrawDataP, viewport.DrawDataBuilder.Layers[1], ImGui.GetBackgroundDrawList(viewport))
|
|
end
|
|
end
|
|
|
|
-- Draw modal/window whitening backgrounds
|
|
ImGui.RenderDimmedBackgrounds()
|
|
|
|
-- Add ImDrawList to render
|
|
local windows_to_render_top_most = {nil, nil}
|
|
windows_to_render_top_most[1] = (g.NavWindowingTarget and (bit32.band(g.NavWindowingTarget.Flags, ImGuiWindowFlags.NoBringToFrontOnFocus) == 0)) and g.NavWindowingTarget.RootWindowDockTree or nil
|
|
windows_to_render_top_most[2] = g.NavWindowingTarget and g.NavWindowingListWindow or nil
|
|
for _, window in g.Windows:iter() do
|
|
if ImGui.IsWindowActiveAndVisible(window) and (bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0) and window ~= windows_to_render_top_most[1] and window ~= windows_to_render_top_most[2] then
|
|
AddRootWindowToDrawData(window)
|
|
end
|
|
end
|
|
for n = 1, 2 do
|
|
if windows_to_render_top_most[n] and ImGui.IsWindowActiveAndVisible(windows_to_render_top_most[n]) then -- NavWindowingTarget is always temporarily displayed as the top-most window
|
|
AddRootWindowToDrawData(windows_to_render_top_most[n])
|
|
end
|
|
end
|
|
|
|
-- Draw software mouse cursor if requested by io.MouseDrawCursor flag
|
|
if g.IO.MouseDrawCursor and g.MouseCursor ~= ImGuiMouseCursor.None then
|
|
ImGui.RenderMouseCursor(g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48))
|
|
end
|
|
|
|
-- Setup ImDrawData structures for end-user
|
|
g.IO.MetricsRenderVertices = 0
|
|
g.IO.MetricsRenderIndices = 0
|
|
for _, viewport in g.Viewports:iter() do
|
|
FlattenDrawDataIntoSingleLayer(viewport.DrawDataBuilder)
|
|
|
|
-- Add foreground ImDrawList (for each active viewport)
|
|
if viewport.BgFgDrawLists[2] ~= nil and viewport.BgFgDrawListsLastTimeActive[2] == g.Time then
|
|
ImGui.AddDrawListToDrawDataEx(viewport.DrawDataP, viewport.DrawDataBuilder.Layers[1], ImGui.GetForegroundDrawList(viewport))
|
|
end
|
|
|
|
-- We call _PopUnusedDrawCmd() last thing, as RenderDimmedBackgrounds() rely on a valid command being there (especially in docking branch).
|
|
local draw_data = viewport.DrawDataP
|
|
for _, draw_list in draw_data.CmdLists:iter() do
|
|
draw_list:_PopUnusedDrawCmd()
|
|
end
|
|
|
|
g.IO.MetricsRenderVertices = g.IO.MetricsRenderVertices + draw_data.TotalVtxCount
|
|
g.IO.MetricsRenderIndices = g.IO.MetricsRenderIndices + draw_data.TotalIdxCount
|
|
end
|
|
end
|
|
|
|
-- Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker.
|
|
-- CalcTextSize("") should return ImVec2(0.0, g.FontSize)
|
|
--- @param text ImString
|
|
--- @param text_end? int # Exclusive upper bound
|
|
--- @param hide_text_after_double_hash? bool
|
|
--- @param wrap_width? float
|
|
--- @return ImVec2
|
|
@native
|
|
function ImGui.CalcTextSize(text, text_end, hide_text_after_double_hash, wrap_width)
|
|
return ImGui.CalcTextSizeEx(text, 1, text_end, hide_text_after_double_hash, wrap_width)
|
|
end
|
|
|
|
--- @param text ImString
|
|
--- @param text_begin int
|
|
--- @param text_end? int # Exclusive upper bound
|
|
--- @param hide_text_after_double_hash? bool
|
|
--- @param wrap_width? float
|
|
--- @return ImVec2
|
|
@native
|
|
function ImGui.CalcTextSizeEx(text, text_begin, text_end, hide_text_after_double_hash, wrap_width)
|
|
if hide_text_after_double_hash == nil then hide_text_after_double_hash = false end
|
|
if wrap_width == nil then wrap_width = -1.0 end
|
|
|
|
local g = GImGui
|
|
|
|
local text_display_end
|
|
if hide_text_after_double_hash then
|
|
text_display_end = ImGui.FindRenderedTextEnd(text, text_end) -- Hide anything after a '##' string
|
|
else
|
|
text_display_end = text_end
|
|
end
|
|
|
|
local font = g.Font
|
|
local font_size = g.FontSize
|
|
if text == "" or (text_end and text_end <= text_begin) then
|
|
return ImVec2(0.0, font_size)
|
|
end
|
|
local text_size = font:CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_begin, text_display_end, nil)
|
|
|
|
-- Round
|
|
-- (see 7b0bf230, 4622fa4b6, #791 for details about this.)
|
|
-- FIXME: Add a way to disable this.
|
|
text_size.x = ImCeilFast(text_size.x)
|
|
|
|
return text_size
|
|
end
|
|
|
|
-- Pass this to your backend rendering function! Valid after Render() and until the next call to NewFrame()
|
|
function ImGui.GetDrawData()
|
|
local g = GImGui
|
|
local viewport = g.Viewports[1]
|
|
return viewport.DrawDataP.Valid and viewport.DrawDataP or nil
|
|
end
|
|
|
|
function ImGui.GetTime()
|
|
return GImGui.Time
|
|
end
|
|
|
|
-- This function is merely here to free heap allocations.
|
|
function ImGui.Shutdown()
|
|
local g = GImGui
|
|
IM_ASSERT_USER_ERROR(g.IO.BackendPlatformUserData == nil, "Forgot to shutdown Platform backend?")
|
|
IM_ASSERT_USER_ERROR(g.IO.BackendRendererUserData == nil, "Forgot to shutdown Renderer backend?")
|
|
for _, viewport in g.Viewports:iter() do
|
|
-- IM_UNUSED(viewport)
|
|
IM_ASSERT_USER_ERROR(viewport.RendererUserData == nil and viewport.PlatformUserData == nil and viewport.PlatformHandle == nil, "Backend or app forgot to call DestroyPlatformWindows()?")
|
|
end
|
|
|
|
-- The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame)
|
|
for _, atlas in g.FontAtlases:iter() do
|
|
ImGui.UnregisterFontAtlas(atlas)
|
|
if atlas.OwnerContext == g then
|
|
IM_ASSERT(atlas.RefCount == 0, "Destroying context owning a ImFontAtlas which is still used elsewhere!")
|
|
if atlas.RefCount == 0 then
|
|
atlas.Locked = false
|
|
end
|
|
end
|
|
end
|
|
g.DrawListSharedData.TempBuffer:clear()
|
|
|
|
-- Shutdown extensions
|
|
ImGui.DockContextShutdown(g)
|
|
|
|
-- Cleanup of other data are conditional on actually having initialized Dear ImGui.
|
|
if not g.Initialized then
|
|
return
|
|
end
|
|
-- TODO:
|
|
|
|
-- Clear everything else
|
|
g.MenusIdSubmittedThisFrame:clear()
|
|
end
|
|
|
|
-- A conformant backend should return nil on failure (e.g. clipboard data is not text).
|
|
--- @return string?
|
|
function ImGui.GetClipboardText()
|
|
local g = GImGui
|
|
return g.PlatformIO.Platform_GetClipboardTextFn and g.PlatformIO.Platform_GetClipboardTextFn(g) or nil
|
|
end
|
|
|
|
--- @param text string
|
|
function ImGui.SetClipboardText(text)
|
|
local g = GImGui
|
|
if g.PlatformIO.Platform_SetClipboardTextFn ~= nil then
|
|
g.PlatformIO.Platform_SetClipboardTextFn(g, text)
|
|
end
|
|
end
|
|
|
|
function ImGui.GetIO() return GImGui.IO end
|
|
|
|
--- @return ImGuiPlatformIO
|
|
function ImGui.GetPlatformIO()
|
|
IM_ASSERT(GImGui ~= nil, "No current context. Did you call ImGui.CreateContext() and ImGui.SetCurrentContext()?")
|
|
return GImGui.PlatformIO
|
|
end
|
|
|
|
-- Get desired mouse cursor shape.
|
|
-- Important: this is meant to be used by a platform backend, it is reset in ImGui.NewFrame(),
|
|
-- updated during the frame, and locked in EndFrame()/Render().
|
|
-- If you use software rendering by setting io.MouseDrawCursor then Dear ImGui will render those for you
|
|
function ImGui.GetMouseCursor()
|
|
local g = GImGui
|
|
return g.MouseCursor
|
|
end
|
|
|
|
-- We intentionally accept values of ImGuiMouseCursor that are outside our bounds, in case users needs to hack-in a custom cursor value.
|
|
-- Custom cursors may be handled by custom backends. If you are using a standard backend and want to hack in a custom cursor, you may
|
|
-- handle it before the backend _NewFrame() call and temporarily set ImGuiConfigFlags.NoMouseCursorChange during the backend _NewFrame() call.
|
|
--- @param cursor_type ImGuiMouseCursor
|
|
function ImGui.SetMouseCursor(cursor_type)
|
|
local g = GImGui
|
|
g.MouseCursor = cursor_type
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] MISC HELPERS/UTILITIES (Color functions)
|
|
-- Note: The Convert functions are early design which are not consistent with other API.
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param col_a ImU32
|
|
--- @param col_b ImU32
|
|
--- @return ImU32
|
|
function ImStd.ImAlphaBlendColors(col_a, col_b)
|
|
local t = bit32.band(bit32.rshift(col_b, IM_COL32_A_SHIFT), 0xFF) / 255.0
|
|
local r = ImLerp(bit32.band(bit32.rshift(col_a, IM_COL32_R_SHIFT), 0xFF), bit32.band(bit32.rshift(col_b, IM_COL32_R_SHIFT), 0xFF), t)
|
|
local g = ImLerp(bit32.band(bit32.rshift(col_a, IM_COL32_G_SHIFT), 0xFF), bit32.band(bit32.rshift(col_b, IM_COL32_G_SHIFT), 0xFF), t)
|
|
local b = ImLerp(bit32.band(bit32.rshift(col_a, IM_COL32_B_SHIFT), 0xFF), bit32.band(bit32.rshift(col_b, IM_COL32_B_SHIFT), 0xFF), t)
|
|
return IM_COL32(r, g, b, 0xFF)
|
|
end
|
|
|
|
--- @param in_col ImU32
|
|
--- @param out_col ImVec4?
|
|
--- @return ImVec4?
|
|
function ImGui.ColorConvertU32ToFloat4(in_col, out_col)
|
|
local s = 1.0 / 255.0
|
|
local r = bit32.band(bit32.rshift(in_col, IM_COL32_R_SHIFT), 0xFF) * s
|
|
local g = bit32.band(bit32.rshift(in_col, IM_COL32_G_SHIFT), 0xFF) * s
|
|
local b = bit32.band(bit32.rshift(in_col, IM_COL32_B_SHIFT), 0xFF) * s
|
|
local a = bit32.band(bit32.rshift(in_col, IM_COL32_A_SHIFT), 0xFF) * s
|
|
|
|
if out_col then
|
|
out_col[1] = r; out_col[2] = g; out_col[3] = b; out_col[4] = a
|
|
else
|
|
return ImVec4(r, g, b, a)
|
|
end
|
|
end
|
|
|
|
--- @param in_col ImVec4
|
|
--- @return ImU32
|
|
local function F32ToU8Sat(v) if v < 0.0 then v = 0.0 elseif v > 1.0 then v = 1.0 end return math.floor(v * 255.0 + 0.5) end
|
|
|
|
@native
|
|
function ImGui.ColorConvertFloat4ToU32(in_col)
|
|
return bit32.bor(F32ToU8Sat(in_col.x), bit32.lshift(F32ToU8Sat(in_col.y), 8), bit32.lshift(F32ToU8Sat(in_col.z), 16), bit32.lshift(F32ToU8Sat(in_col.w), 24))
|
|
end
|
|
|
|
-- Convert rgb floats ([0-1],[0-1],[0-1]) to hsv floats ([0-1],[0-1],[0-1]), from Foley & van Dam p592
|
|
-- Optimized http://lolengine.net/blog/2013/01/13/fast-rgb-to-hsv
|
|
--- @param r float
|
|
--- @param g float
|
|
--- @param b float
|
|
--- @return float, float, float
|
|
@native
|
|
function ImGui.ColorConvertRGBtoHSV(r, g, b)
|
|
local K = 0.0
|
|
if g < b then
|
|
g, b = b, g
|
|
K = -1.0
|
|
end
|
|
if r < g then
|
|
r, g = g, r
|
|
K = -2.0 / 6.0 - K
|
|
end
|
|
|
|
local chroma = r - (g < b and g or b)
|
|
local out_h = ImFabs(K + (g - b) / (6.0 * chroma + 1e-20))
|
|
local out_s = chroma / (r + 1e-20)
|
|
local out_v = r
|
|
return out_h, out_s, out_v
|
|
end
|
|
|
|
-- Convert hsv floats ([0-1],[0-1],[0-1]) to rgb floats ([0-1],[0-1],[0-1]), from Foley & van Dam p593
|
|
-- also http://en.wikipedia.org/wiki/HSL_and_HSV
|
|
--- @param h float
|
|
--- @param s float
|
|
--- @param v float
|
|
--- @return float, float, float
|
|
@native
|
|
function ImGui.ColorConvertHSVtoRGB(h, s, v)
|
|
if s == 0.0 then
|
|
-- gray
|
|
return v, v, v
|
|
end
|
|
|
|
h = ImFmod(h, 1.0) / (60.0 / 360.0)
|
|
local i = math.floor(h)
|
|
local f = h - i
|
|
local p = v * (1.0 - s)
|
|
local q = v * (1.0 - s * f)
|
|
local t = v * (1.0 - s * (1.0 - f))
|
|
|
|
if i == 0 then
|
|
return v, t, p
|
|
elseif i == 1 then
|
|
return q, v, p
|
|
elseif i == 2 then
|
|
return p, v, t
|
|
elseif i == 3 then
|
|
return p, q, v
|
|
elseif i == 4 then
|
|
return t, p, v
|
|
else -- i == 5 or default
|
|
return v, p, q
|
|
end
|
|
end
|
|
|
|
-- Generic helper, equivalent to old ImGui.CalcListClipping() but stateless
|
|
--- @param clip_rect ImRect
|
|
--- @param pos ImVec2
|
|
--- @param items_height float
|
|
function ImGui.CalcClipRectVisibleItemsY(clip_rect, pos, items_height)
|
|
local visible_start = ImMax(ImTrunc((clip_rect.Min.y - pos.y) / items_height), 0)
|
|
local visible_end = ImMax(ImTrunc(ImCeil((clip_rect.Max.y - pos.y) / items_height)), visible_start)
|
|
return visible_start + 1, visible_end + 1
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] STYLING
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.GetStyle()
|
|
IM_ASSERT(GImGui ~= nil, "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?")
|
|
return GImGui.Style
|
|
end
|
|
|
|
do
|
|
|
|
local c = ImVec4()
|
|
|
|
--- @param col ImGuiCol|ImVec4|ImU32
|
|
--- @param alpha_mul? float
|
|
--- @param col_is_u32? bool # must be truthy if `col` is `ImU32`
|
|
--- @return ImU32
|
|
@native
|
|
function ImGui.GetColorU32(col, alpha_mul, col_is_u32)
|
|
if alpha_mul == nil then alpha_mul = 1.0 end
|
|
|
|
local style = GImGui.Style
|
|
|
|
if not col_is_u32 then
|
|
--- @cast col ImVec4|ImGuiCol
|
|
local w
|
|
if type(col) == "number" then
|
|
col = style.Colors[col]
|
|
w = col.w * style.Alpha * alpha_mul
|
|
else --- @cast col ImVec4
|
|
w = col.w * style.Alpha
|
|
end
|
|
return bit32.bor(F32ToU8Sat(col.x), bit32.lshift(F32ToU8Sat(col.y), 8), bit32.lshift(F32ToU8Sat(col.z), 16), bit32.lshift(F32ToU8Sat(w), 24))
|
|
else
|
|
--- @cast col ImU32
|
|
alpha_mul = alpha_mul * style.Alpha
|
|
if alpha_mul >= 1.0 then
|
|
return col
|
|
end
|
|
local a = bit32.rshift(bit32.band(col, IM_COL32_A_MASK), IM_COL32_A_SHIFT)
|
|
a = math.floor(a * alpha_mul) -- We don't need to clamp 0..255 because alpha is in 0..1 range.
|
|
return bit32.bor(bit32.band(col, bit32.bnot(IM_COL32_A_MASK)), bit32.lshift(a, IM_COL32_A_SHIFT))
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32
|
|
--- @param idx ImGuiCol
|
|
--- @param col ImU32|ImVec4
|
|
function ImGui.PushStyleColor(idx, col)
|
|
local g = GImGui
|
|
local backup = ImGuiColorMod(idx, g.Style.Colors[idx])
|
|
g.ColorStack:push_back(backup)
|
|
if g.DebugFlashStyleColorIdx ~= idx then
|
|
if type(col) == "number" then
|
|
ImGui.ColorConvertU32ToFloat4(col, g.Style.Colors[idx])
|
|
else --- @cast col ImVec4
|
|
ImVec4_Copy(g.Style.Colors[idx], col)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param count? int
|
|
function ImGui.PopStyleColor(count)
|
|
if count == nil then count = 1 end
|
|
|
|
local g = GImGui
|
|
if g.ColorStack.Size < count then
|
|
IM_ASSERT_USER_ERROR(false, "Calling PopStyleColor() too many times!")
|
|
count = g.ColorStack.Size
|
|
end
|
|
|
|
while count > 0 do
|
|
local backup = g.ColorStack:back()
|
|
ImVec4_Copy(g.Style.Colors[backup.Col], backup.BackupValue)
|
|
g.ColorStack:pop_back()
|
|
count = count - 1
|
|
end
|
|
end
|
|
|
|
local GStyleVarsInfo = {
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "Alpha"), -- ImGuiStyleVar.Alpha
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "DisabledAlpha"), -- ImGuiStyleVar.DisabledAlpha
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "WindowPadding"), -- ImGuiStyleVar.WindowPadding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "WindowRounding"), -- ImGuiStyleVar.WindowRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "WindowBorderSize"), -- ImGuiStyleVar.WindowBorderSize
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "WindowMinSize"), -- ImGuiStyleVar.WindowMinSize
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "WindowTitleAlign"), -- ImGuiStyleVar.WindowTitleAlign
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ChildRounding"), -- ImGuiStyleVar.ChildRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ChildBorderSize"), -- ImGuiStyleVar.ChildBorderSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "PopupRounding"), -- ImGuiStyleVar.PopupRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "PopupBorderSize"), -- ImGuiStyleVar.PopupBorderSize
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "FramePadding"), -- ImGuiStyleVar.FramePadding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "FrameRounding"), -- ImGuiStyleVar.FrameRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "FrameBorderSize"), -- ImGuiStyleVar.FrameBorderSize
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "ItemSpacing"), -- ImGuiStyleVar.ItemSpacing
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "ItemInnerSpacing"), -- ImGuiStyleVar.ItemInnerSpacing
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "IndentSpacing"), -- ImGuiStyleVar.IndentSpacing
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "CellPadding"), -- ImGuiStyleVar.CellPadding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ScrollbarSize"), -- ImGuiStyleVar.ScrollbarSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ScrollbarRounding"), -- ImGuiStyleVar.ScrollbarRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ScrollbarPadding"), -- ImGuiStyleVar.ScrollbarPadding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "GrabMinSize"), -- ImGuiStyleVar.GrabMinSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "GrabRounding"), -- ImGuiStyleVar.GrabRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ImageRounding"), -- ImGuiStyleVar.ImageRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "ImageBorderSize"), -- ImGuiStyleVar.ImageBorderSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TabRounding"), -- ImGuiStyleVar.TabRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TabBorderSize"), -- ImGuiStyleVar.TabBorderSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TabMinWidthBase"), -- ImGuiStyleVar.TabMinWidthBase
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TabMinWidthShrink"), -- ImGuiStyleVar.TabMinWidthShrink
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TabBarBorderSize"), -- ImGuiStyleVar.TabBarBorderSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TabBarOverlineSize"), -- ImGuiStyleVar.TabBarOverlineSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TableAngledHeadersAngle"), -- ImGuiStyleVar.TableAngledHeadersAngle
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "TableAngledHeadersTextAlign"), -- ImGuiStyleVar.TableAngledHeadersTextAlign
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TreeLinesSize"), -- ImGuiStyleVar.TreeLinesSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "TreeLinesRounding"), -- ImGuiStyleVar.TreeLinesRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "MenuItemRounding"), -- ImGuiStyleVar.MenuItemRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "SelectableRounding"), -- ImGuiStyleVar.SelectableRounding
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "DragDropTargetRounding"), -- ImGuiStyleVar.DragDropTargetRounding
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "ButtonTextAlign"), -- ImGuiStyleVar.ButtonTextAlign
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "SelectableTextAlign"), -- ImGuiStyleVar.SelectableTextAlign
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "SeparatorSize"), -- ImGuiStyleVar.SeparatorSize
|
|
ImGuiStyleVarInfo(1, ImGuiDataType.Float, "SeparatorTextBorderSize"), -- ImGuiStyleVar.SeparatorTextBorderSize
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "SeparatorTextAlign"), -- ImGuiStyleVar.SeparatorTextAlign
|
|
ImGuiStyleVarInfo(2, ImGuiDataType.Float, "SeparatorTextPadding") -- ImGuiStyleVar.SeparatorTextPadding
|
|
}
|
|
|
|
--- @param idx ImGuiStyleVar
|
|
--- @return ImGuiStyleVarInfo
|
|
function ImGui.GetStyleVarInfo(idx)
|
|
IM_ASSERT(idx >= 0 and idx < ImGuiStyleVar.COUNT)
|
|
-- IM_STATIC_ASSERT(IM_COUNTOF(GStyleVarsInfo) == ImGuiStyleVar.COUNT)
|
|
return GStyleVarsInfo[idx + 1]
|
|
end
|
|
|
|
--- @param idx ImGuiStyleVar
|
|
--- @param val float|ImVec2
|
|
function ImGui.PushStyleVar(idx, val)
|
|
local g = GImGui
|
|
local var_info = ImGui.GetStyleVarInfo(idx)
|
|
if type(val) == "number" then
|
|
IM_ASSERT_USER_ERROR_RET(var_info.DataType == ImGuiDataType.Float and var_info.Count == 1, "Calling PushStyleVar() with wrong type!")
|
|
else -- ImVec2
|
|
IM_ASSERT_USER_ERROR_RET(var_info.DataType == ImGuiDataType.Float and var_info.Count == 2, "Calling PushStyleVar() with wrong type!")
|
|
end
|
|
local var = g.Style[var_info.Key]
|
|
g.StyleVarStack:push_back(ImGuiStyleMod(idx, var))
|
|
if type(val) == "number" then
|
|
g.Style[var_info.Key] = val
|
|
else -- copy into the style's own ImVec2: storing the caller's vector would alias it into the style forever
|
|
var.x = val.x; var.y = val.y
|
|
end
|
|
end
|
|
|
|
--- @param idx ImGuiStyleVar
|
|
--- @param val_x float
|
|
function ImGui.PushStyleVarX(idx, val_x)
|
|
local g = GImGui
|
|
local var_info = ImGui.GetStyleVarInfo(idx)
|
|
IM_ASSERT_USER_ERROR_RET(var_info.DataType == ImGuiDataType.Float and var_info.Count == 2, "Calling PushStyleVarX() with wrong type!")
|
|
local pvar = g.Style[var_info.Key]
|
|
g.StyleVarStack:push_back(ImGuiStyleMod(idx, pvar))
|
|
pvar.x = val_x
|
|
end
|
|
|
|
--- @param idx ImGuiStyleVar
|
|
--- @param val_y float
|
|
function ImGui.PushStyleVarY(idx, val_y)
|
|
local g = GImGui
|
|
local var_info = ImGui.GetStyleVarInfo(idx)
|
|
IM_ASSERT_USER_ERROR_RET(var_info.DataType == ImGuiDataType.Float and var_info.Count == 2, "Calling PushStyleVarY() with wrong type!")
|
|
local pvar = g.Style[var_info.Key]
|
|
g.StyleVarStack:push_back(ImGuiStyleMod(idx, pvar))
|
|
pvar.y = val_y
|
|
end
|
|
|
|
--- @param count? int
|
|
function ImGui.PopStyleVar(count)
|
|
if count == nil then count = 1 end
|
|
|
|
local g = GImGui
|
|
if g.StyleVarStack.Size < count then
|
|
IM_ASSERT_USER_ERROR(0, "Calling PopStyleVar() too many times!")
|
|
count = g.StyleVarStack.Size
|
|
end
|
|
while count > 0 do
|
|
local backup = g.StyleVarStack:back()
|
|
local var_info = ImGui.GetStyleVarInfo(backup.VarIdx)
|
|
local data = g.Style[var_info.Key]
|
|
if (var_info.DataType == ImGuiDataType.Float and var_info.Count == 1) then
|
|
g.Style[var_info.Key] = backup.BackupVal[1]
|
|
elseif (var_info.DataType == ImGuiDataType.Float and var_info.Count == 2) then
|
|
data.x = backup.BackupVal[1]; data.y = backup.BackupVal[2]
|
|
end
|
|
g.StyleVarStack:pop_back()
|
|
count = count - 1
|
|
end
|
|
end
|
|
|
|
-- ATTENTION THIS IS IN LEGACY LOCAL SPACE.
|
|
--- @param wrap_local_pos_x float
|
|
function ImGui.PushTextWrapPos(wrap_local_pos_x)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
window.DC.TextWrapPosStack:push_back(window.DC.TextWrapPos)
|
|
window.DC.TextWrapPos = wrap_local_pos_x
|
|
end
|
|
|
|
function ImGui.PopTextWrapPos()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT_USER_ERROR_RET(window.DC.TextWrapPosStack.Size > 0, "Calling PopTextWrapPos() too many times!")
|
|
window.DC.TextWrapPos = window.DC.TextWrapPosStack:back()
|
|
window.DC.TextWrapPosStack:pop_back()
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param popup_hierarchy bool
|
|
--- @param dock_hierarchy bool
|
|
local function GetCombinedRootWindow(window, popup_hierarchy, dock_hierarchy)
|
|
local last_window = nil
|
|
while last_window ~= window do
|
|
last_window = window
|
|
window = window.RootWindow
|
|
if popup_hierarchy then
|
|
window = window.RootWindowPopupTree
|
|
end
|
|
if dock_hierarchy then
|
|
window = window.RootWindowDockTree
|
|
end
|
|
end
|
|
return window
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param potential_parent ImGuiWindow
|
|
--- @param popup_hierarchy bool
|
|
--- @param dock_hierarchy bool
|
|
function ImGui.IsWindowChildOf(window, potential_parent, popup_hierarchy, dock_hierarchy)
|
|
local window_root = GetCombinedRootWindow(window, popup_hierarchy, dock_hierarchy)
|
|
if window_root == potential_parent then
|
|
return true
|
|
end
|
|
|
|
while window ~= nil do
|
|
if window == potential_parent then
|
|
return true
|
|
end
|
|
if window == window_root then -- end of chain
|
|
return false
|
|
end
|
|
window = window.ParentWindow
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
function ImGui.IsWindowInBeginStack(window)
|
|
local g = GImGui
|
|
for n = g.CurrentWindowStack.Size, 1, -1 do
|
|
if g.CurrentWindowStack.Data[n].Window == window then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param potential_parent ImGuiWindow
|
|
function ImGui.IsWindowWithinBeginStackOf(window, potential_parent)
|
|
if window.RootWindow == potential_parent then
|
|
return true
|
|
end
|
|
|
|
while window ~= nil do
|
|
if window == potential_parent then
|
|
return true
|
|
end
|
|
window = window.ParentWindowInBeginStack
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
--- @param potential_above ImGuiWindow
|
|
--- @param potential_below ImGuiWindow
|
|
--- @return bool
|
|
function ImGui.IsWindowAbove(potential_above, potential_below)
|
|
local g = GImGui
|
|
|
|
-- It would be saner to ensure that display layer is always reflected in the g.Windows[] order, which would likely requires altering all manipulations of that array
|
|
local display_layer_delta = GetWindowDisplayLayer(potential_above) - GetWindowDisplayLayer(potential_below)
|
|
if display_layer_delta ~= 0 then
|
|
return display_layer_delta > 0
|
|
end
|
|
|
|
for i = g.Windows.Size, 1, -1 do
|
|
local candidate_window = g.Windows.Data[i]
|
|
if candidate_window == potential_above then
|
|
return true
|
|
end
|
|
if candidate_window == potential_below then
|
|
return false
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags for options.
|
|
-- IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app,
|
|
-- you should not use this function! Use the 'io.WantCaptureMouse' boolean for that!
|
|
-- Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details.
|
|
--- @param flags? ImGuiHoveredFlags
|
|
function ImGui.IsWindowHovered(flags)
|
|
if flags == nil then flags = 0 end
|
|
local g = GImGui
|
|
IM_ASSERT_USER_ERROR(bit32.band(flags, bit32.bnot(ImGuiHoveredFlags.AllowedMaskForIsWindowHovered)) == 0, "Invalid flags for IsWindowHovered()!")
|
|
|
|
local ref_window = g.HoveredWindow
|
|
local cur_window = g.CurrentWindow
|
|
if ref_window == nil then
|
|
return false
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiHoveredFlags.AnyWindow) == 0 then
|
|
IM_ASSERT(cur_window) -- Not inside a Begin()/End()
|
|
local popup_hierarchy = bit32.band(flags, ImGuiHoveredFlags.NoPopupHierarchy) == 0
|
|
local dock_hierarchy = bit32.band(flags, ImGuiHoveredFlags.DockHierarchy) ~= 0
|
|
if bit32.band(flags, ImGuiHoveredFlags.RootWindow) ~= 0 then
|
|
cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy, dock_hierarchy)
|
|
end
|
|
|
|
local result
|
|
if bit32.band(flags, ImGuiHoveredFlags.ChildWindows) ~= 0 then
|
|
result = ImGui.IsWindowChildOf(ref_window, cur_window, popup_hierarchy, dock_hierarchy)
|
|
else
|
|
result = (ref_window == cur_window)
|
|
end
|
|
if not result then
|
|
return false
|
|
end
|
|
end
|
|
|
|
if not ImGui.IsWindowContentHoverable(ref_window, flags) then
|
|
return false
|
|
end
|
|
if bit32.band(flags, ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) == 0 then
|
|
if g.ActiveId ~= 0 and not g.ActiveIdAllowOverlap and g.ActiveId ~= ref_window.MoveId then
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- When changing hovered window we requires a bit of stationary delay before activating hover timer.
|
|
-- FIXME: We don't support delay other than stationary one for now, other delay would need a way
|
|
-- to fulfill the possibility that multiple IsWindowHovered() with varying flag could return true
|
|
-- for different windows of the hierarchy. Possibly need a Hash(Current+Flags) ==> (Timer) cache.
|
|
-- We can implement this for _Stationary because the data is linked to HoveredWindow rather than CurrentWindow.
|
|
if bit32.band(flags, ImGuiHoveredFlags.ForTooltip) ~= 0 then
|
|
flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse)
|
|
end
|
|
if bit32.band(flags, ImGuiHoveredFlags.Stationary) ~= 0 and g.HoverWindowUnlockedStationaryId ~= ref_window.ID then
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- Similar to IsWindowHovered()
|
|
--- @param flags? ImGuiFocusedFlags
|
|
function ImGui.IsWindowFocused(flags)
|
|
if flags == nil then flags = 0 end
|
|
local g = GImGui
|
|
local ref_window = g.NavWindow
|
|
local cur_window = g.CurrentWindow
|
|
|
|
if ref_window == nil then
|
|
return false
|
|
end
|
|
if bit32.band(flags, ImGuiFocusedFlags.AnyWindow) ~= 0 then
|
|
return true
|
|
end
|
|
|
|
IM_ASSERT(cur_window) -- Not inside a Begin()/End()
|
|
local popup_hierarchy = bit32.band(flags, ImGuiFocusedFlags.NoPopupHierarchy) == 0
|
|
local dock_hierarchy = bit32.band(flags, ImGuiFocusedFlags.DockHierarchy) ~= 0
|
|
if bit32.band(flags, ImGuiFocusedFlags.RootWindow) ~= 0 then
|
|
cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy, dock_hierarchy)
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiFocusedFlags.ChildWindows) ~= 0 then
|
|
return ImGui.IsWindowChildOf(ref_window, cur_window, popup_hierarchy, dock_hierarchy)
|
|
else
|
|
return ref_window == cur_window
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param scale float
|
|
local function ScaleWindow(window, scale)
|
|
local origin = window.Viewport.Pos
|
|
ImVec2_Copy(window.Pos, ImFloor((window.Pos - origin) * scale + origin))
|
|
ImVec2_Copy(window.Size, ImTrunc(window.Size * scale))
|
|
ImVec2_Copy(window.SizeFull, ImTrunc(window.SizeFull * scale))
|
|
ImVec2_Copy(window.ContentSize, ImTrunc(window.ContentSize * scale))
|
|
end
|
|
|
|
-- Scale all windows (position, size). Use when e.g. changing DPI. (This is a lossy operation!)
|
|
--- @param viewport ImGuiViewportP
|
|
--- @param scale float
|
|
function ImGui.ScaleWindowsInViewport(viewport, scale)
|
|
local g = GImGui
|
|
-- IMGUI_DEBUG_LOG_VIEWPORT("[viewport] ScaleWindowsInViewport 0x%08X\n", viewport.ID)
|
|
|
|
if viewport.Window then
|
|
ScaleWindow(viewport.Window, scale)
|
|
else
|
|
for _, window in g.Windows:iter() do
|
|
if window.Viewport == viewport then
|
|
ScaleWindow(window, scale)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] SCROLLING
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Helper to snap on edges when aiming at an item very close to the edge,
|
|
-- So the difference between WindowPadding and ItemSpacing will be in the visible area after scrolling.
|
|
-- When we refactor the scrolling API this may be configurable with a flag?
|
|
-- Note that the effect for this won't be visible on X axis with default Style settings as WindowPadding.x == ItemSpacing.x by default.
|
|
--- @param target float
|
|
--- @param snap_min float
|
|
--- @param snap_max float
|
|
--- @param snap_threshold float
|
|
--- @param center_ratio float
|
|
local function CalcScrollEdgeSnap(target, snap_min, snap_max, snap_threshold, center_ratio)
|
|
if target <= snap_min + snap_threshold then
|
|
return ImLerp(snap_min, target, center_ratio)
|
|
end
|
|
if target >= snap_max - snap_threshold then
|
|
return ImLerp(target, snap_max, center_ratio)
|
|
end
|
|
return target
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
function CalcNextScrollFromScrollTargetAndClamp(window)
|
|
local scroll = ImVec2()
|
|
ImVec2_Copy(scroll, window.Scroll)
|
|
local decoration_size = ImVec2(window.DecoOuterSizeX1 + window.DecoInnerSizeX1 + window.DecoOuterSizeX2, window.DecoOuterSizeY1 + window.DecoInnerSizeY1 + window.DecoOuterSizeY2)
|
|
|
|
for axis = ImGuiAxis.X, ImGuiAxis.Y do
|
|
if window.ScrollTarget[axis] < FLT_MAX then
|
|
local center_ratio = window.ScrollTargetCenterRatio[axis]
|
|
local scroll_target = window.ScrollTarget[axis]
|
|
|
|
if window.ScrollTargetEdgeSnapDist[axis] > 0.0 then
|
|
local snap_min = 0.0
|
|
local snap_max = window.ScrollMax[axis] + window.SizeFull[axis] - decoration_size[axis]
|
|
scroll_target = CalcScrollEdgeSnap(scroll_target, snap_min, snap_max, window.ScrollTargetEdgeSnapDist[axis], center_ratio)
|
|
end
|
|
|
|
scroll[axis] = scroll_target - center_ratio * (window.SizeFull[axis] - decoration_size[axis])
|
|
end
|
|
scroll[axis] = ImRound64(ImMax(scroll[axis], 0.0))
|
|
if not window.Collapsed and not window.SkipItems then
|
|
scroll[axis] = ImMin(scroll[axis], window.ScrollMax[axis])
|
|
end
|
|
end
|
|
|
|
return scroll
|
|
end
|
|
|
|
-- Scroll to keep newly navigated item fully into view
|
|
--- @param window ImGuiWindow
|
|
--- @param item_rect ImRect
|
|
--- @param flags? ImGuiScrollFlags
|
|
function ImGui.ScrollToRectEx(window, item_rect, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui
|
|
local scroll_rect = ImRect(window.InnerRect.Min - ImVec2(1, 1), window.InnerRect.Max + ImVec2(1, 1))
|
|
scroll_rect.Min.x = ImMin(scroll_rect.Min.x + window.DecoInnerSizeX1, scroll_rect.Max.x)
|
|
scroll_rect.Min.y = ImMin(scroll_rect.Min.y + window.DecoInnerSizeY1, scroll_rect.Max.y)
|
|
-- GetForegroundDrawList(window):AddRect(item_rect.Min, item_rect.Max, IM_COL32(255,0,0,255), 0.0, 0, 5.0) -- [DEBUG]
|
|
-- GetForegroundDrawList(window):AddRect(scroll_rect.Min, scroll_rect.Max, IM_COL32_WHITE) -- [DEBUG]
|
|
|
|
-- Check that only one behavior is selected per axis
|
|
IM_ASSERT(bit32.band(flags, ImGuiScrollFlags.MaskX_) == 0 or ImIsPowerOfTwo(bit32.band(flags, ImGuiScrollFlags.MaskX_)))
|
|
IM_ASSERT(bit32.band(flags, ImGuiScrollFlags.MaskY_) == 0 or ImIsPowerOfTwo(bit32.band(flags, ImGuiScrollFlags.MaskY_)))
|
|
|
|
-- Defaults
|
|
local in_flags = flags
|
|
if bit32.band(flags, ImGuiScrollFlags.MaskX_) == 0 and window.ScrollbarX then
|
|
flags = bit32.bor(flags, ImGuiScrollFlags.KeepVisibleEdgeX)
|
|
end
|
|
if bit32.band(flags, ImGuiScrollFlags.MaskY_) == 0 then
|
|
flags = bit32.bor(flags, window.Appearing and ImGuiScrollFlags.AlwaysCenterY or ImGuiScrollFlags.KeepVisibleEdgeY)
|
|
end
|
|
|
|
local fully_visible_x = item_rect.Min.x >= scroll_rect.Min.x and item_rect.Max.x <= scroll_rect.Max.x
|
|
local fully_visible_y = item_rect.Min.y >= scroll_rect.Min.y and item_rect.Max.y <= scroll_rect.Max.y
|
|
local can_be_fully_visible_x = (item_rect:GetWidth() + g.Style.ItemSpacing.x * 2.0) <= scroll_rect:GetWidth() or (window.AutoFitFramesX > 0) or bit32.band(window.Flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0
|
|
local can_be_fully_visible_y = (item_rect:GetHeight() + g.Style.ItemSpacing.y * 2.0) <= scroll_rect:GetHeight() or (window.AutoFitFramesY > 0) or bit32.band(window.Flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0
|
|
|
|
if bit32.band(flags, ImGuiScrollFlags.KeepVisibleEdgeX) ~= 0 and not fully_visible_x then
|
|
if item_rect.Min.x < scroll_rect.Min.x or not can_be_fully_visible_x then
|
|
ImGui.SetScrollFromPosX(window, item_rect.Min.x - g.Style.ItemSpacing.x - window.Pos.x, 0.0)
|
|
elseif item_rect.Max.x >= scroll_rect.Max.x then
|
|
ImGui.SetScrollFromPosX(window, item_rect.Max.x + g.Style.ItemSpacing.x - window.Pos.x, 1.0)
|
|
end
|
|
elseif (bit32.band(flags, ImGuiScrollFlags.KeepVisibleCenterX) ~= 0 and not fully_visible_x) or bit32.band(flags, ImGuiScrollFlags.AlwaysCenterX) ~= 0 then
|
|
if can_be_fully_visible_x then
|
|
ImGui.SetScrollFromPosX(window, ImTrunc((item_rect.Min.x + item_rect.Max.x) * 0.5) - window.Pos.x, 0.5)
|
|
else
|
|
ImGui.SetScrollFromPosX(window, item_rect.Min.x - window.Pos.x, 0.0)
|
|
end
|
|
end
|
|
|
|
if bit32.band(flags, ImGuiScrollFlags.KeepVisibleEdgeY) ~= 0 and not fully_visible_y then
|
|
if item_rect.Min.y < scroll_rect.Min.y or not can_be_fully_visible_y then
|
|
ImGui.SetScrollFromPosY(window, item_rect.Min.y - g.Style.ItemSpacing.y - window.Pos.y, 0.0)
|
|
elseif item_rect.Max.y >= scroll_rect.Max.y then
|
|
ImGui.SetScrollFromPosY(window, item_rect.Max.y + g.Style.ItemSpacing.y - window.Pos.y, 1.0)
|
|
end
|
|
elseif (bit32.band(flags, ImGuiScrollFlags.KeepVisibleCenterY) ~= 0 and not fully_visible_y) or bit32.band(flags, ImGuiScrollFlags.AlwaysCenterY) ~= 0 then
|
|
if can_be_fully_visible_y then
|
|
ImGui.SetScrollFromPosY(window, ImTrunc((item_rect.Min.y + item_rect.Max.y) * 0.5) - window.Pos.y, 0.5)
|
|
else
|
|
ImGui.SetScrollFromPosY(window, item_rect.Min.y - window.Pos.y, 0.0)
|
|
end
|
|
end
|
|
|
|
local next_scroll = CalcNextScrollFromScrollTargetAndClamp(window)
|
|
local delta_scroll = next_scroll - window.Scroll
|
|
|
|
-- Also scroll parent window to keep us into view if necessary
|
|
if bit32.band(flags, ImGuiScrollFlags.NoScrollParent) == 0 and bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
-- FIXME-SCROLL: May be an option?
|
|
if bit32.band(in_flags, bit32.bor(ImGuiScrollFlags.AlwaysCenterX, ImGuiScrollFlags.KeepVisibleCenterX)) ~= 0 then
|
|
in_flags = bit32.bor(bit32.band(in_flags, bit32.bnot(ImGuiScrollFlags.MaskX_)), ImGuiScrollFlags.KeepVisibleEdgeX)
|
|
end
|
|
if bit32.band(in_flags, bit32.bor(ImGuiScrollFlags.AlwaysCenterY, ImGuiScrollFlags.KeepVisibleCenterY)) ~= 0 then
|
|
in_flags = bit32.bor(bit32.band(in_flags, bit32.bnot(ImGuiScrollFlags.MaskY_)), ImGuiScrollFlags.KeepVisibleEdgeY)
|
|
end
|
|
local parent_delta = ImGui.ScrollToRectEx(window.ParentWindow, ImRect(item_rect.Min - delta_scroll, item_rect.Max - delta_scroll), in_flags)
|
|
delta_scroll = delta_scroll + parent_delta
|
|
end
|
|
|
|
return delta_scroll
|
|
end
|
|
|
|
--- @return float
|
|
function ImGui.GetScrollMaxY()
|
|
local window = GImGui.CurrentWindow
|
|
return window.ScrollMax.y
|
|
end
|
|
|
|
function ImGui.GetScrollMaxX()
|
|
return GImGui.CurrentWindow.ScrollMax.x
|
|
end
|
|
|
|
function ImGui.GetScrollX()
|
|
return GImGui.CurrentWindow.Scroll.x
|
|
end
|
|
|
|
function ImGui.GetScrollY()
|
|
return GImGui.CurrentWindow.Scroll.y
|
|
end
|
|
|
|
--- @param flags? ImGuiScrollFlags
|
|
function ImGui.ScrollToItem(flags)
|
|
local g = GImGui
|
|
ImGui.ScrollToRectEx(g.CurrentWindow, g.LastItemData.NavRect, flags or 0)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param item_rect ImRect
|
|
--- @param flags? ImGuiScrollFlags
|
|
function ImGui.ScrollToRect(window, item_rect, flags)
|
|
ImGui.ScrollToRectEx(window, item_rect, flags or 0)
|
|
end
|
|
|
|
-- center_x_ratio: 0.0 left of last item, 0.5 horizontal center of last item, 1.0 right of last item.
|
|
--- @param center_x_ratio? float
|
|
function ImGui.SetScrollHereX(center_x_ratio)
|
|
if center_x_ratio == nil then center_x_ratio = 0.5 end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local spacing_x = ImMax(window.WindowPadding.x, g.Style.ItemSpacing.x)
|
|
local target_pos_x = ImLerp(g.LastItemData.Rect.Min.x - spacing_x, g.LastItemData.Rect.Max.x + spacing_x, center_x_ratio)
|
|
ImGui.SetScrollFromPosX(window, target_pos_x - window.Pos.x, center_x_ratio) -- Convert from absolute to local pos
|
|
|
|
-- Tweak: snap on edges when aiming at an item very close to the edge
|
|
window.ScrollTargetEdgeSnapDist.x = ImMax(0.0, window.WindowPadding.x - spacing_x)
|
|
end
|
|
|
|
-- center_y_ratio: 0.0 top of last item, 0.5 vertical center of last item, 1.0 bottom of last item.
|
|
--- @param center_y_ratio? float
|
|
function ImGui.SetScrollHereY(center_y_ratio)
|
|
if center_y_ratio == nil then center_y_ratio = 0.5 end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local spacing_y = ImMax(window.WindowPadding.y, g.Style.ItemSpacing.y)
|
|
local target_pos_y = ImLerp(window.DC.CursorPosPrevLine.y - spacing_y, window.DC.CursorPosPrevLine.y + window.DC.PrevLineSize.y + spacing_y, center_y_ratio)
|
|
ImGui.SetScrollFromPosY(window, target_pos_y - window.Pos.y, center_y_ratio) -- Convert from absolute to local pos
|
|
|
|
-- Tweak: snap on edges when aiming at an item very close to the edge
|
|
window.ScrollTargetEdgeSnapDist.y = ImMax(0.0, window.WindowPadding.y - spacing_y)
|
|
end
|
|
|
|
-- Overloads: SetScrollX(scroll_x) / SetScrollX(window, scroll_x)
|
|
--- @param window ImGuiWindow|float
|
|
--- @param scroll_x float?
|
|
function ImGui.SetScrollX(window, scroll_x)
|
|
if type(window) == "number" then window, scroll_x = GImGui.CurrentWindow, window end
|
|
window.ScrollTarget.x = scroll_x
|
|
window.ScrollTargetCenterRatio.x = 0.0
|
|
window.ScrollTargetEdgeSnapDist.x = 0.0
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param scroll_y float
|
|
function ImGui.SetScrollY(window, scroll_y)
|
|
if type(window) == "number" then window, scroll_y = GImGui.CurrentWindow, window end
|
|
window.ScrollTarget.y = scroll_y
|
|
window.ScrollTargetCenterRatio.y = 0.0
|
|
window.ScrollTargetEdgeSnapDist.y = 0.0
|
|
end
|
|
|
|
-- Note that a local position will vary depending on initial scroll value,
|
|
-- This is a little bit confusing so bear with us:
|
|
-- - local_pos = (absolution_pos - window.Pos)
|
|
-- - So local_x/local_y are 0.0 for a position at the upper-left corner of a window,
|
|
-- and generally local_x/local_y are >(padding+decoration) && <(size-padding-decoration) when in the visible area.
|
|
-- - They mostly exist because of legacy API.
|
|
-- Following the rules above, when trying to work with scrolling code, consider that:
|
|
-- - SetScrollFromPosY(0.0) == SetScrollY(0.0 + scroll.y) == has no effect!
|
|
-- - SetScrollFromPosY(-scroll.y) == SetScrollY(-scroll.y + scroll.y) == SetScrollY(0.0) == reset scroll. Of course writing SetScrollY(0.0) directly then makes more sense
|
|
-- We store a target position so centering and clamping can occur on the next frame when we are guaranteed to have a known window size
|
|
--- @param window ImGuiWindow
|
|
--- @param local_x float
|
|
--- @param center_x_ratio float
|
|
function ImGui.SetScrollFromPosX(window, local_x, center_x_ratio)
|
|
if type(window) == "number" then window, local_x, center_x_ratio = GImGui.CurrentWindow, window, local_x end
|
|
if center_x_ratio == nil then center_x_ratio = 0.5 end
|
|
IM_ASSERT(center_x_ratio >= 0.0 and center_x_ratio <= 1.0)
|
|
window.ScrollTarget.x = IM_TRUNC(local_x - window.DecoOuterSizeX1 - window.DecoInnerSizeX1 + window.Scroll.x) -- Convert local position to scroll offset
|
|
window.ScrollTargetCenterRatio.x = center_x_ratio
|
|
window.ScrollTargetEdgeSnapDist.x = 0.0
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param local_y float
|
|
--- @param center_y_ratio float
|
|
function ImGui.SetScrollFromPosY(window, local_y, center_y_ratio)
|
|
if type(window) == "number" then window, local_y, center_y_ratio = GImGui.CurrentWindow, window, local_y end
|
|
if center_y_ratio == nil then center_y_ratio = 0.5 end
|
|
IM_ASSERT(center_y_ratio >= 0.0 and center_y_ratio <= 1.0)
|
|
window.ScrollTarget.y = IM_TRUNC(local_y - window.DecoOuterSizeY1 - window.DecoInnerSizeY1 + window.Scroll.y) -- Convert local position to scroll offset
|
|
window.ScrollTargetCenterRatio.y = center_y_ratio
|
|
window.ScrollTargetEdgeSnapDist.y = 0.0
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] TOOLTIPS
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param tooltip_flags ImGuiTooltipFlags
|
|
--- @param extra_window_flags ImGuiWindowFlags
|
|
function ImGui.BeginTooltipEx(tooltip_flags, extra_window_flags)
|
|
local g = GImGui
|
|
|
|
local is_dragdrop_tooltip = g.DragDropWithinSource or g.DragDropWithinTarget
|
|
if is_dragdrop_tooltip then
|
|
-- Drag and Drop tooltips are positioning differently than other tooltips:
|
|
-- - offset visibility to increase visibility around mouse.
|
|
-- - never clamp within outer viewport boundary.
|
|
-- We call SetNextWindowPos() to enforce position and disable clamping.
|
|
-- See FindBestWindowPosForPopup() for positioning logic of other tooltips (not drag and drop ones).
|
|
-- local tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding
|
|
local is_touchscreen = (g.IO.MouseSource == ImGuiMouseSource.TouchScreen)
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasPos) == 0 then
|
|
local tooltip_pos
|
|
if is_touchscreen then
|
|
tooltip_pos = g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET_TOUCH * g.Style.MouseCursorScale
|
|
else
|
|
tooltip_pos = g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET_MOUSE * g.Style.MouseCursorScale
|
|
end
|
|
|
|
local tooltip_pivot = is_touchscreen and TOOLTIP_DEFAULT_PIVOT_TOUCH or ImVec2(0.0, 0.0)
|
|
ImGui.SetNextWindowPos(tooltip_pos, ImGuiCond.None, tooltip_pivot)
|
|
end
|
|
|
|
local bg_alpha = g.Style.Colors[ImGuiCol.PopupBg].w * 0.60
|
|
ImGui.SetNextWindowBgAlpha(bg_alpha)
|
|
-- PushStyleVar(ImGuiStyleVar.Alpha, g.Style.Alpha * 0.60) -- This would be nice but e.g ColorButton with checkerboard has issue with transparent colors :(
|
|
|
|
tooltip_flags = bit32.bor(tooltip_flags, ImGuiTooltipFlags.OverridePrevious)
|
|
end
|
|
|
|
-- Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one.
|
|
if (bit32.band(tooltip_flags, ImGuiTooltipFlags.OverridePrevious) ~= 0) and g.TooltipPreviousWindow ~= nil and g.TooltipPreviousWindow.Active and not ImGui.IsWindowInBeginStack(g.TooltipPreviousWindow) then
|
|
-- IMGUI_DEBUG_LOG("[tooltip] '%s' already active, using +1 for this frame\n", window_name)
|
|
ImGui.SetWindowHiddenAndSkipItemsForCurrentFrame(g.TooltipPreviousWindow)
|
|
g.TooltipOverrideCount = g.TooltipOverrideCount + 1
|
|
end
|
|
|
|
local window_name_template = is_dragdrop_tooltip and "##Tooltip_DragDrop_%02d" or "##Tooltip_%02d"
|
|
local window_name = ImFormatString(window_name_template, g.TooltipOverrideCount)
|
|
|
|
local flags = bit32.bor(ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.NoInputs, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoMove, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoSavedSettings, ImGuiWindowFlags.AlwaysAutoResize)
|
|
|
|
ImGui.Begin(window_name, nil, bit32.bor(flags, extra_window_flags))
|
|
-- 2023-03-09: Added bool return value to the API, but currently always returning true.
|
|
-- If this ever returns false we need to update BeginDragDropSource() accordingly.
|
|
-- if (!ret)
|
|
-- End();
|
|
-- return ret;
|
|
return true
|
|
end
|
|
|
|
function ImGui.BeginTooltip()
|
|
return ImGui.BeginTooltipEx(ImGuiTooltipFlags.None, ImGuiWindowFlags.None)
|
|
end
|
|
|
|
function ImGui.BeginItemTooltip()
|
|
if not ImGui.IsItemHovered(ImGuiHoveredFlags.ForTooltip) then
|
|
return false
|
|
end
|
|
return ImGui.BeginTooltipEx(ImGuiTooltipFlags.None, ImGuiWindowFlags.None)
|
|
end
|
|
|
|
function ImGui.EndTooltip()
|
|
IM_ASSERT(bit32.band(ImGui.GetCurrentWindowRead().Flags, ImGuiWindowFlags.Tooltip) ~= 0) -- Mismatched BeginTooltip()/EndTooltip() calls
|
|
ImGui.End()
|
|
end
|
|
|
|
--- @param fmt string
|
|
--- @param ... any
|
|
function ImGui.SetTooltip(fmt, ...)
|
|
if not ImGui.BeginTooltipEx(ImGuiTooltipFlags.OverridePrevious, ImGuiWindowFlags.None) then
|
|
return
|
|
end
|
|
ImGui.TextV(fmt, ...)
|
|
ImGui.EndTooltip()
|
|
end
|
|
|
|
-- Shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav'.
|
|
-- Defaults to == ImGuiHoveredFlags.Stationary | ImGuiHoveredFlags.DelayShort when using the mouse.
|
|
--- @param fmt string
|
|
--- @param ... any
|
|
function ImGui.SetItemTooltip(fmt, ...)
|
|
if ImGui.IsItemHovered(ImGuiHoveredFlags.ForTooltip) then
|
|
ImGui.SetTooltip(fmt, ...)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] POPUPS
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags.NoFocusOnAppearing)
|
|
-- should be positioned behind that modal window, unless the window was created inside the modal begin-stack.
|
|
-- In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent.
|
|
-- - WindowA -- FindBlockingModal() returns Modal1
|
|
-- - WindowB -- .. returns Modal1
|
|
-- - Modal1 -- .. returns Modal2
|
|
-- - WindowC -- .. returns Modal2
|
|
-- - WindowD -- .. returns Modal2
|
|
-- - Modal2 -- .. returns Modal2
|
|
-- - WindowE -- .. returns nil
|
|
-- Notes:
|
|
-- - FindBlockingModal(nil) == nil is generally equivalent to GetTopMostPopupModal() == nil.
|
|
-- Only difference is here we check for .Active/WasActive but it may be unnecessary.
|
|
--- @param window? ImGuiWindow
|
|
function ImGui.FindBlockingModal(window)
|
|
local g = GImGui
|
|
if g.OpenPopupStack.Size <= 0 then
|
|
return nil
|
|
end
|
|
|
|
-- Find a modal that has common parent with specified window. Specified window should be positioned behind that modal.
|
|
for i = 1, g.OpenPopupStack.Size do
|
|
repeat
|
|
local popup_data = g.OpenPopupStack.Data[i]
|
|
local popup_window = popup_data.Window
|
|
if popup_window == nil or bit32.band(popup_window.Flags, ImGuiWindowFlags.Modal) == 0 then
|
|
break
|
|
end
|
|
if not popup_window.Active and not popup_window.WasActive then -- Check WasActive, because this code may run before popup renders on current frame, also check Active to handle newly created windows.
|
|
break
|
|
end
|
|
if window == nil then -- FindBlockingModal(nil) test for if FocusWindow(nil) is naturally possible via a mouse click.
|
|
return popup_window
|
|
end
|
|
if ImGui.IsWindowWithinBeginStackOf(window, popup_window) then -- Window may be over modal
|
|
break
|
|
end
|
|
|
|
do return popup_window end -- Place window right below first block modal
|
|
|
|
until true
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- @param id ImGuiID|string
|
|
--- @param popup_flags? ImGuiPopupFlags
|
|
function ImGui.OpenPopup(id, popup_flags)
|
|
if popup_flags == nil then popup_flags = 0 end
|
|
|
|
if type(id) == "number" then
|
|
return ImGui.OpenPopupEx(id, popup_flags)
|
|
else --- @cast id string
|
|
local g = GImGui
|
|
local _id = g.CurrentWindow:GetID(id)
|
|
IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopup(\"%s\" -> 0x%08X)", id, _id)
|
|
return ImGui.OpenPopupEx(_id, popup_flags)
|
|
end
|
|
end
|
|
|
|
-- Mark popup as open (toggle toward open state).
|
|
-- - Return true when the popup is toggled open, which allows you to capture local state if needed.
|
|
-- You may also call IsWindowAppearing() inside the later BeginPopup() scope if you need to prepare/compute data for the popup.
|
|
-- - Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block.
|
|
-- - Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level).
|
|
-- - One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to nil).
|
|
--- @param id ImGuiID
|
|
--- @param popup_flags ImGuiPopupFlags
|
|
function ImGui.OpenPopupEx(id, popup_flags)
|
|
local g = GImGui
|
|
local parent_window = g.CurrentWindow
|
|
local current_stack_size = g.BeginPopupStack.Size
|
|
|
|
if bit32.band(popup_flags, ImGuiPopupFlags.NoOpenOverExistingPopup) ~= 0 then
|
|
if ImGui.IsPopupOpen(0, ImGuiPopupFlags.AnyPopupId) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local popup_ref = ImGuiPopupData() -- Tagged as new ref as Window will be set back to nil if we write this into OpenPopupStack.
|
|
popup_ref.PopupId = id
|
|
popup_ref.Window = nil
|
|
popup_ref.RestoreNavWindow = g.NavWindow -- When popup closes focus may be restored to NavWindow (depend on window type).
|
|
popup_ref.OpenFrameCount = g.FrameCount
|
|
popup_ref.OpenParentId = parent_window.IDStack:back()
|
|
ImVec2_Copy(popup_ref.OpenPopupPos, ImGui.NavCalcPreferredRefPos(ImGuiWindowFlags.Popup))
|
|
if ImGui.IsMousePosValid(g.IO.MousePos) then
|
|
ImVec2_Copy(popup_ref.OpenMousePos, g.IO.MousePos)
|
|
else
|
|
ImVec2_Copy(popup_ref.OpenMousePos, popup_ref.OpenPopupPos)
|
|
end
|
|
|
|
-- IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopupEx(0x%08X)\n", id)
|
|
if g.OpenPopupStack.Size < current_stack_size + 1 then
|
|
g.OpenPopupStack:push_back(popup_ref)
|
|
return true
|
|
else
|
|
-- Gently handle the user mistakenly calling OpenPopup() every frames: it is likely a programming mistake!
|
|
-- However, if we were to run the regular code path, the ui would become completely unusable because the popup will always be
|
|
-- in hidden-while-calculating-size state _while_ claiming focus. Which is extremely confusing situation for the programmer.
|
|
-- Instead, for successive frames calls to OpenPopup(), we silently avoid reopening even if ImGuiPopupFlags.NoReopen is not specified.
|
|
|
|
local keep_existing = false
|
|
if g.OpenPopupStack.Data[current_stack_size + 1].PopupId == id then
|
|
if (g.OpenPopupStack.Data[current_stack_size + 1].OpenFrameCount == g.FrameCount - 1) or (bit32.band(popup_flags, ImGuiPopupFlags.NoReopen) ~= 0) then
|
|
keep_existing = true
|
|
end
|
|
end
|
|
|
|
if keep_existing then
|
|
-- No reopen
|
|
g.OpenPopupStack.Data[current_stack_size + 1].OpenFrameCount = popup_ref.OpenFrameCount
|
|
else
|
|
-- Reopen: close child popups if any, then flag popup for open/reopen (set position, focus, init navigation)
|
|
ImGui.ClosePopupToLevel(current_stack_size, true)
|
|
g.OpenPopupStack:push_back(popup_ref)
|
|
end
|
|
|
|
-- When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow().
|
|
-- This is equivalent to what ClosePopupToLevel() does.
|
|
-- if (g.OpenPopupStack[current_stack_size + 1].PopupId == id)
|
|
-- FocusWindow(parent_window);
|
|
|
|
return not keep_existing
|
|
end
|
|
end
|
|
|
|
-- When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it.
|
|
-- This function closes any popups that are over 'ref_window'.
|
|
--- @param ref_window? ImGuiWindow
|
|
--- @param restore_focus_to_window_under_popup bool
|
|
function ImGui.ClosePopupsOverWindow(ref_window, restore_focus_to_window_under_popup)
|
|
local g = GImGui
|
|
if g.OpenPopupStack.Size == 0 then
|
|
return
|
|
end
|
|
|
|
-- Don't close our own child popup windows.
|
|
-- IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\") restore_under=%d\n", ref_window and ref_window.Name or "<NULL>", restore_focus_to_window_under_popup)
|
|
local popup_count_to_keep = 0
|
|
if ref_window then
|
|
-- Find the highest popup which is a descendant of the reference window (generally reference window = NavWindow)
|
|
local ref_window_is_descendent_of_popup
|
|
local stop_8041 = false
|
|
while popup_count_to_keep < g.OpenPopupStack.Size do
|
|
repeat
|
|
local popup = g.OpenPopupStack.Data[popup_count_to_keep + 1]
|
|
if not popup.Window then
|
|
break
|
|
end
|
|
|
|
IM_ASSERT(bit32.band(popup.Window.Flags, ImGuiWindowFlags.Popup) ~= 0)
|
|
|
|
-- Trim the stack unless the popup is a direct parent of the reference window (the reference window is often the NavWindow)
|
|
-- - Clicking/Focusing Window2 won't close Popup1:
|
|
-- Window -> Popup1 -> Window2(Ref)
|
|
-- - Clicking/focusing Popup1 will close Popup2 and Popup3:
|
|
-- Window -> Popup1(Ref) -> Popup2 -> Popup3
|
|
-- - Each popups may contain child windows, which is why we compare .RootWindowDockTree!
|
|
-- Window -> Popup1 -> Popup1_Child -> Popup2 -> Popup2_Child
|
|
-- We step through every popup from bottom to top to validate their position relative to reference window.
|
|
ref_window_is_descendent_of_popup = false
|
|
for n = popup_count_to_keep, g.OpenPopupStack.Size - 1 do
|
|
local popup_window = g.OpenPopupStack.Data[n + 1].Window
|
|
-- if (popup_window.RootWindowDockTree == ref_window.RootWindowDockTree) -- FIXME-MERGE
|
|
if popup_window and ImGui.IsWindowWithinBeginStackOf(ref_window, popup_window) then
|
|
ref_window_is_descendent_of_popup = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not ref_window_is_descendent_of_popup then
|
|
stop_8041 = true; break
|
|
end
|
|
|
|
until true
|
|
if stop_8041 then break end
|
|
|
|
popup_count_to_keep = popup_count_to_keep + 1
|
|
end
|
|
end
|
|
if popup_count_to_keep < g.OpenPopupStack.Size then -- This test is not required but it allows to set a convenient breakpoint on the statement below
|
|
-- IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\")\n", ref_window and ref_window.Name or "<NULL>")
|
|
ImGui.ClosePopupToLevel(popup_count_to_keep, restore_focus_to_window_under_popup)
|
|
end
|
|
end
|
|
|
|
--- @param remaining int # 0-based
|
|
--- @param restore_focus_to_window_under_popup bool
|
|
function ImGui.ClosePopupToLevel(remaining, restore_focus_to_window_under_popup)
|
|
local g = GImGui
|
|
-- IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupToLevel(%d), restore_under=%d\n", remaining, restore_focus_to_window_under_popup)
|
|
IM_ASSERT(remaining >= 0 and remaining < g.OpenPopupStack.Size)
|
|
-- if bit32.band(g.DebugLogFlags, ImGuiDebugLogFlags.EventPopup) ~= 0 then
|
|
-- for n = remaining + 1, g.OpenPopupStack.Size do
|
|
-- local popup = g.OpenPopupStack.Data[n]
|
|
-- IMGUI_DEBUG_LOG_POPUP("[popup] - Closing PopupID 0x%08X Window \"%s\"", popup.PopupId, popup.Window and popup.Window.Name or nil)
|
|
-- end
|
|
-- end
|
|
|
|
-- Trim open popup stack
|
|
-- FIXME: is it safer to full copy here?
|
|
local prev_popup = g.OpenPopupStack.Data[remaining + 1]
|
|
g.OpenPopupStack:resize(remaining)
|
|
|
|
-- Restore focus (unless popup window was not yet submitted, and didn't have a chance to take focus anyhow. See #7325 for an edge case)
|
|
if restore_focus_to_window_under_popup and prev_popup.Window then
|
|
local popup_window = prev_popup.Window
|
|
local focus_window
|
|
if bit32.band(popup_window.Flags, ImGuiWindowFlags.ChildMenu) ~= 0 then
|
|
focus_window = popup_window.ParentWindow
|
|
else
|
|
focus_window = prev_popup.RestoreNavWindow
|
|
end
|
|
|
|
if focus_window and not focus_window.WasActive then
|
|
ImGui.FocusTopMostWindowUnderOne(popup_window, nil, nil, ImGuiFocusRequestFlags.RestoreFocusedChild) -- Fallback
|
|
else
|
|
local focus_flags = (g.NavLayer == ImGuiNavLayer.Main) and ImGuiFocusRequestFlags.RestoreFocusedChild or ImGuiFocusRequestFlags.None
|
|
ImGui.FocusWindow(focus_window, focus_flags)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Close the popup we have begin-ed into.
|
|
function ImGui.CloseCurrentPopup()
|
|
local g = GImGui
|
|
local popup_idx = g.BeginPopupStack.Size
|
|
if popup_idx < 1 or popup_idx > g.OpenPopupStack.Size or g.BeginPopupStack.Data[popup_idx].PopupId ~= g.OpenPopupStack.Data[popup_idx].PopupId then
|
|
return
|
|
end
|
|
|
|
-- Closing a menu closes its top-most parent popup (unless a modal)
|
|
while popup_idx > 1 do
|
|
local popup_window = g.OpenPopupStack.Data[popup_idx].Window
|
|
local parent_popup_window = g.OpenPopupStack.Data[popup_idx - 1].Window
|
|
local close_parent = false
|
|
if popup_window and bit32.band(popup_window.Flags, ImGuiWindowFlags.ChildMenu) ~= 0 then
|
|
if parent_popup_window and not (bit32.band(parent_popup_window.Flags, ImGuiWindowFlags.MenuBar) ~= 0) then
|
|
close_parent = true
|
|
end
|
|
end
|
|
|
|
if not close_parent then
|
|
break
|
|
end
|
|
|
|
popup_idx = popup_idx - 1
|
|
end
|
|
|
|
-- IMGUI_DEBUG_LOG_POPUP("[popup] CloseCurrentPopup %d -> %d\n", g.BeginPopupStack.Size, popup_idx)
|
|
ImGui.ClosePopupToLevel(popup_idx - 1, true)
|
|
|
|
-- A common pattern is to close a popup when selecting a menu item/selectable that will open another window.
|
|
-- To improve this usage pattern, we avoid nav highlight for a single frame in the parent window.
|
|
-- Similarly, we could avoid mouse hover highlight in this window but it is less visually problematic.
|
|
local window = g.NavWindow
|
|
if window then
|
|
window.DC.NavHideHighlightOneFrame = true
|
|
end
|
|
end
|
|
|
|
-- Attention! BeginPopup() adds default flags when calling BeginPopupEx()!
|
|
--- @param id ImGuiID
|
|
--- @param extra_window_flags ImGuiWindowFlags
|
|
function ImGui.BeginPopupEx(id, extra_window_flags)
|
|
local g = GImGui
|
|
if not ImGui.IsPopupOpen(id, ImGuiPopupFlags.None) then
|
|
g.NextWindowData:ClearFlags() -- We behave like Begin() and need to consume those values
|
|
return false
|
|
end
|
|
|
|
IM_ASSERT(bit32.band(extra_window_flags, ImGuiWindowFlags.ChildMenu) == 0) -- Use BeginPopupMenuEx()
|
|
local name = ImFormatString("##Popup_%08x", id) -- No recycling, so we can close/open during the same frame
|
|
|
|
local is_open
|
|
_, is_open = ImGui.Begin(name, nil, bit32.bor(extra_window_flags, ImGuiWindowFlags.Popup, ImGuiWindowFlags.NoDocking))
|
|
|
|
if not is_open then -- NB: Begin can return false when the popup is completely clipped (e.g. zero size display)
|
|
ImGui.EndPopup()
|
|
end
|
|
-- g.CurrentWindow.FocusRouteParentWindow = g.CurrentWindow.ParentWindowInBeginStack
|
|
return is_open
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
--- @param label any
|
|
--- @param extra_window_flags ImGuiWindowFlags
|
|
function ImGui.BeginPopupMenuEx(id, label, extra_window_flags)
|
|
local g = GImGui
|
|
if not ImGui.IsPopupOpen(id, ImGuiPopupFlags.None) then
|
|
g.NextWindowData:ClearFlags() -- We behave like Begin() and need to consume those values
|
|
return false
|
|
end
|
|
|
|
-- As we bypass BeginChild(), set ImGuiChildFlags.AlwaysAutoResize as it is checked independently from ImGuiWindowFlags.AlwaysAutoResize for now (see #9355)
|
|
-- Ideally we should remove setting ImGuiWindowFlags.AlwaysAutoResize in BeginChild().
|
|
if bit32.band(extra_window_flags, ImGuiWindowFlags.ChildWindow) ~= 0 and bit32.band(extra_window_flags, ImGuiWindowFlags.AlwaysAutoResize) ~= 0 then
|
|
ImGui.SetNextWindowChildFlags(ImGuiChildFlags.AlwaysAutoResize, true)
|
|
end
|
|
|
|
IM_ASSERT(bit32.band(extra_window_flags, ImGuiWindowFlags.ChildMenu) ~= 0)
|
|
local name = ImFormatString("%s###Menu_%02d", label, g.BeginMenuDepth) -- Recycle windows based on depth
|
|
local is_open
|
|
_, is_open = ImGui.Begin(name, nil, bit32.bor(extra_window_flags, ImGuiWindowFlags.Popup))
|
|
if not is_open then -- NB: Begin can return false when the popup is completely clipped (e.g. zero size display)
|
|
ImGui.EndPopup()
|
|
end
|
|
-- g.CurrentWindow.FocusRouteParentWindow = g.CurrentWindow.ParentWindowInBeginStack
|
|
return is_open
|
|
end
|
|
|
|
--- @param str_id string
|
|
--- @param flags? ImGuiWindowFlags
|
|
function ImGui.BeginPopup(str_id, flags)
|
|
if flags == nil then flags = 0 end
|
|
|
|
local g = GImGui
|
|
if g.OpenPopupStack.Size <= g.BeginPopupStack.Size then -- Early out for performance
|
|
g.NextWindowData:ClearFlags() -- We behave like Begin() and need to consume those values
|
|
return false
|
|
end
|
|
flags = bit32.bor(flags, ImGuiWindowFlags.AlwaysAutoResize, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoSavedSettings)
|
|
local id = g.CurrentWindow:GetID(str_id)
|
|
return ImGui.BeginPopupEx(id, flags)
|
|
end
|
|
|
|
function ImGui.EndPopup()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT_USER_ERROR_RET((bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0) and (g.BeginPopupStack.Size > 0), "Calling EndPopup() in wrong window!")
|
|
|
|
-- Make all menus and popups wrap around for now, may need to expose that policy (e.g. focus scope could include wrap/loop policy flags used by new move requests)
|
|
if g.NavWindow == window then
|
|
ImGui.NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags.LoopY)
|
|
end
|
|
|
|
-- Child-popups don't need to be laid out
|
|
local backup_within_end_popup_id = g.WithinEndPopupID
|
|
local backup_within_end_child_id = g.WithinEndChildID
|
|
g.WithinEndPopupID = window.ID
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
g.WithinEndChildID = window.ID
|
|
end
|
|
|
|
ImGui.End()
|
|
g.WithinEndPopupID = backup_within_end_popup_id
|
|
g.WithinEndChildID = backup_within_end_child_id
|
|
end
|
|
|
|
-- r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.)
|
|
-- r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it.
|
|
-- (r_outer is usually equivalent to the viewport rectangle minus padding, but when multi-viewports are enabled and monitor
|
|
-- information are available, it may represent the entire platform monitor from the frame of reference of the current viewport.
|
|
-- this allows us to have tooltips/popups displayed out of the parent viewport.)
|
|
--- @param ref_pos ImVec2
|
|
--- @param size ImVec2
|
|
--- @param last_dir ImGuiDir
|
|
--- @param r_outer ImRect
|
|
--- @param r_avoid ImRect
|
|
--- @param policy ImGuiPopupPositionPolicy
|
|
--- @return ImVec2 # Best Window Pos For Popup
|
|
--- @return int # Updated last_dir
|
|
--- @nodiscard
|
|
function ImGui.FindBestWindowPosForPopupEx(ref_pos, size, last_dir, r_outer, r_avoid, policy)
|
|
local base_pos_clamped
|
|
|
|
-- Combo Box policy (we want a connecting edge)
|
|
if policy == ImGuiPopupPositionPolicy.ComboBox then
|
|
local dir_preferred_order = {ImGuiDir.Down, ImGuiDir.Right, ImGuiDir.Left, ImGuiDir.Up}
|
|
|
|
for n = (last_dir ~= ImGuiDir.None) and 0 or 1, ImGuiDir.COUNT do
|
|
repeat
|
|
local dir = (n == 0) and last_dir or dir_preferred_order[n]
|
|
|
|
if n ~= 0 and dir == last_dir then -- Already tried this direction?
|
|
break
|
|
end
|
|
|
|
local pos
|
|
if dir == ImGuiDir.Down then pos = ImVec2(r_avoid.Min.x, r_avoid.Max.y) -- Below, Toward Right (default)
|
|
elseif dir == ImGuiDir.Right then pos = ImVec2(r_avoid.Min.x, r_avoid.Min.y - size.y) -- Above, Toward Right
|
|
elseif dir == ImGuiDir.Left then pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Max.y) -- Below, Toward Left
|
|
elseif dir == ImGuiDir.Up then pos = ImVec2(r_avoid.Max.x - size.x, r_avoid.Min.y - size.y) -- Above, Toward Left
|
|
end
|
|
|
|
if not r_outer:Contains(ImRect(pos, pos + size)) then
|
|
break
|
|
end
|
|
|
|
last_dir = dir
|
|
do return pos, last_dir end
|
|
|
|
until true
|
|
end
|
|
end
|
|
|
|
-- Tooltip and Default popup policy
|
|
-- (Always first try the direction we used on the last frame, if any)
|
|
if policy == ImGuiPopupPositionPolicy.Tooltip or policy == ImGuiPopupPositionPolicy.Default then
|
|
local dir_preferred_order = {ImGuiDir.Right, ImGuiDir.Down, ImGuiDir.Up, ImGuiDir.Left}
|
|
|
|
for n = (last_dir ~= ImGuiDir.None) and 0 or 1, ImGuiDir.COUNT do
|
|
repeat
|
|
local dir = (n == 0) and last_dir or dir_preferred_order[n]
|
|
|
|
if n ~= 0 and dir == last_dir then -- Already tried this direction?
|
|
break
|
|
end
|
|
|
|
local avail_w = ((dir == ImGuiDir.Left) and r_avoid.Min.x or r_outer.Max.x) - ((dir == ImGuiDir.Right) and r_avoid.Max.x or r_outer.Min.x)
|
|
local avail_h = ((dir == ImGuiDir.Up) and r_avoid.Min.y or r_outer.Max.y) - ((dir == ImGuiDir.Down) and r_avoid.Max.y or r_outer.Min.y)
|
|
|
|
-- If there's not enough room on one axis, there's no point in positioning on a side on this axis (e.g. when not enough width, use a top/bottom position to maximize available width)
|
|
if avail_w < size.x and (dir == ImGuiDir.Left or dir == ImGuiDir.Right) then
|
|
break
|
|
end
|
|
|
|
if avail_h < size.y and (dir == ImGuiDir.Up or dir == ImGuiDir.Down) then
|
|
break
|
|
end
|
|
|
|
base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size)
|
|
local pos = ImVec2()
|
|
pos.x = (dir == ImGuiDir.Left) and (r_avoid.Min.x - size.x) or ((dir == ImGuiDir.Right) and r_avoid.Max.x or base_pos_clamped.x)
|
|
pos.y = (dir == ImGuiDir.Up) and (r_avoid.Min.y - size.y) or ((dir == ImGuiDir.Down) and r_avoid.Max.y or base_pos_clamped.y)
|
|
|
|
-- Clamp top-left corner of popup
|
|
pos.x = ImMax(pos.x, r_outer.Min.x)
|
|
pos.y = ImMax(pos.y, r_outer.Min.y)
|
|
|
|
last_dir = dir
|
|
do return pos, last_dir end
|
|
|
|
until true
|
|
end
|
|
end
|
|
|
|
-- Fallback when not enough room:
|
|
last_dir = ImGuiDir.None
|
|
|
|
-- For tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible.
|
|
if policy == ImGuiPopupPositionPolicy.Tooltip then
|
|
return ref_pos + ImVec2(2, 2), last_dir
|
|
end
|
|
|
|
-- Otherwise try to keep within display
|
|
local pos = ImVec2(ref_pos.x, ref_pos.y)
|
|
pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x)
|
|
pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y)
|
|
return pos, last_dir
|
|
end
|
|
|
|
-- Note that this is used for popups, which can overlap the non work-area of individual viewports.
|
|
--- @param window ImGuiWindow
|
|
--- @return ImRect
|
|
--- @nodiscard
|
|
function ImGui.GetPopupAllowedExtentRect(window)
|
|
local g = GImGui
|
|
local r_screen = ImRect()
|
|
|
|
if window.ViewportAllowPlatformMonitorExtend >= 1 then
|
|
-- Extent with be in the frame of reference of the given viewport (so Min is likely to be negative here)
|
|
local monitor = g.PlatformIO.Monitors.Data[window.ViewportAllowPlatformMonitorExtend]
|
|
ImVec2_Copy(r_screen.Min, monitor.WorkPos)
|
|
ImVec2_Copy(r_screen.Max, monitor.WorkPos + monitor.WorkSize)
|
|
else
|
|
-- Use the full viewport area (not work area) for popups
|
|
r_screen = window.Viewport:GetMainRect()
|
|
end
|
|
|
|
local padding = g.Style.DisplaySafeAreaPadding
|
|
r_screen:Expand(ImVec2((r_screen:GetWidth() > padding.x * 2) and -padding.x or 0.0, (r_screen:GetHeight() > padding.y * 2) and -padding.y or 0.0))
|
|
|
|
return r_screen
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
function ImGui.FindBestWindowPosForPopup(window)
|
|
local g = GImGui
|
|
|
|
local r_outer = ImGui.GetPopupAllowedExtentRect(window)
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildMenu) ~= 0 then
|
|
-- Child menus typically request _any_ position within the parent menu item, and then we move the new menu outside the parent bounds.
|
|
-- This is how we end up with child menus appearing (most-commonly) on the right of the parent menu.
|
|
local parent_window = window.ParentWindow
|
|
local horizontal_overlap = g.Style.ItemInnerSpacing.x -- We want some overlap to convey the relative depth of each menu (currently the amount of overlap is hard-coded to style.ItemSpacing.x).
|
|
|
|
local r_avoid
|
|
if parent_window.DC.MenuBarAppending then
|
|
r_avoid = ImRect(-FLT_MAX, parent_window.ClipRect.Min.y, FLT_MAX, parent_window.ClipRect.Max.y) -- Avoid parent menu-bar. If we wanted multi-line menu-bar, we may instead want to have the calling window setup e.g. a NextWindowData.PosConstraintAvoidRect field
|
|
else
|
|
r_avoid = ImRect(parent_window.Pos.x + horizontal_overlap, -FLT_MAX, parent_window.Pos.x + parent_window.Size.x - horizontal_overlap - parent_window.ScrollbarSizes.x, FLT_MAX)
|
|
end
|
|
|
|
local pos
|
|
pos, window.AutoPosLastDirection = ImGui.FindBestWindowPosForPopupEx(window.Pos, window.Size, window.AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy.Default)
|
|
return pos
|
|
end
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
local pos
|
|
pos, window.AutoPosLastDirection = ImGui.FindBestWindowPosForPopupEx(window.Pos, window.Size, window.AutoPosLastDirection, r_outer, ImRect(window.Pos, window.Pos), ImGuiPopupPositionPolicy.Default) -- Ideally we'd disable r_avoid here
|
|
return pos
|
|
end
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Tooltip) ~= 0 then
|
|
-- Position tooltip (always follows mouse + clamp within outer boundaries)
|
|
-- FIXME:
|
|
-- - Too many paths. One problem is that FindBestWindowPosForPopupEx() doesn't allow passing a suggested position (so touch screen path doesn't use it by default).
|
|
-- - Drag and drop tooltips are not using this path either: BeginTooltipEx() manually sets their position.
|
|
-- - Require some tidying up. In theory we could handle both cases in same location, but requires a bit of shuffling
|
|
-- as drag and drop tooltips are calling SetNextWindowPos() leading to 'window_pos_set_by_api' being set in Begin().
|
|
IM_ASSERT(g.CurrentWindow == window)
|
|
local scale = g.Style.MouseCursorScale
|
|
local ref_pos = ImGui.NavCalcPreferredRefPos(ImGuiWindowFlags.Tooltip)
|
|
|
|
if g.IO.MouseSource == ImGuiMouseSource.TouchScreen and ImGui.NavCalcPreferredRefPosSource(ImGuiWindowFlags.Tooltip) == ImGuiInputSource.Mouse then
|
|
local tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET_TOUCH * scale - TOOLTIP_DEFAULT_PIVOT_TOUCH * window.Size
|
|
if r_outer:Contains(ImRect(tooltip_pos, tooltip_pos + window.Size)) then
|
|
return tooltip_pos
|
|
end
|
|
end
|
|
|
|
local tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET_MOUSE * scale
|
|
local r_avoid
|
|
if g.NavCursorVisible and g.NavHighlightItemUnderNav and not g.IO.ConfigNavMoveSetMousePos then
|
|
r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8)
|
|
else
|
|
r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * scale, ref_pos.y + 24 * scale) -- FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important.
|
|
end
|
|
-- ImGui.GetForegroundDrawList():AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255, 0, 255, 255))
|
|
|
|
local pos
|
|
pos, window.AutoPosLastDirection = ImGui.FindBestWindowPosForPopupEx(tooltip_pos, window.Size, window.AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy.Tooltip)
|
|
return pos
|
|
end
|
|
|
|
IM_ASSERT(false)
|
|
|
|
return window.Pos
|
|
end
|
|
|
|
--- @param flags ImGuiPopupFlags
|
|
--- @return ImGuiMouseButton
|
|
function ImGui.GetMouseButtonFromPopupFlags(flags)
|
|
IM_ASSERT(bit32.band(flags, ImGuiPopupFlags.InvalidMask_) == 0)
|
|
if bit32.band(flags, ImGuiPopupFlags.MouseButtonMask_) ~= 0 then
|
|
return bit32.rshift(bit32.band(flags, ImGuiPopupFlags.MouseButtonMask_), ImGuiPopupFlags.MouseButtonShift_) - 1
|
|
end
|
|
|
|
return ImGuiMouseButton.Right -- Default == 1
|
|
end
|
|
|
|
--- @param popup_flags ImGuiPopupFlags
|
|
--- @param id ImGuiID
|
|
@native
|
|
function ImGui.IsPopupOpenRequestForItem(popup_flags, id)
|
|
local g = GImGui
|
|
local mouse_button = ImGui.GetMouseButtonFromPopupFlags(popup_flags)
|
|
if ImGui.IsMouseReleased(mouse_button) and ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByPopup) then
|
|
return true
|
|
end
|
|
if g.NavOpenContextMenuItemId == id and (ImGui.IsItemFocused() or id == g.CurrentWindow.MoveId) then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Helper to open a popup if mouse button is released over the item
|
|
-- - This is essentially the same as BeginPopupContextItem() but without the trailing BeginPopup()
|
|
--- @param str_id? string
|
|
--- @param popup_flags? ImGuiPopupFlags
|
|
@native
|
|
function ImGui.OpenPopupOnItemClick(str_id, popup_flags)
|
|
if popup_flags == nil then popup_flags = 0 end
|
|
|
|
local g = GImGui
|
|
if ImGui.IsPopupOpenRequestForItem(popup_flags, g.LastItemData.ID) then
|
|
local window = g.CurrentWindow
|
|
local id
|
|
if str_id then id = window:GetID(str_id) else id = g.LastItemData.ID end -- If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict!
|
|
IM_ASSERT(id ~= 0) -- You cannot pass a nil str_id if the last item has no identifier (e.g. a Text() item)
|
|
return ImGui.OpenPopupEx(id, popup_flags)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- This is a helper to handle the simplest case of associating one named popup to one given widget.
|
|
-- - To create a popup associated to the last item, you generally want to pass a nil value to str_id.
|
|
-- - To create a popup with a specific identifier, pass it in str_id.
|
|
-- - This is useful when using BeginPopupContextItem() on an item which doesn't have an identifier, e.g. a Text() call.
|
|
-- - This is useful when multiple code locations may want to manipulate/open the same popup, given an explicit id.
|
|
-- - You may want to handle the whole on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters).
|
|
-- This is essentially the same as:
|
|
-- id = str_id ? GetID(str_id) : GetItemID();
|
|
-- OpenPopupOnItemClick(str_id, ImGuiPopupFlags_MouseButtonRight);
|
|
-- return BeginPopup(id);
|
|
-- Which is essentially the same as:
|
|
-- id = str_id ? GetID(str_id) : GetItemID();
|
|
-- if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right))
|
|
-- OpenPopup(id);
|
|
-- return BeginPopup(id);
|
|
-- The main difference being that this is tweaked to avoid computing the ID twice.
|
|
--- @param str_id? string
|
|
--- @param popup_flags? ImGuiPopupFlags
|
|
--- @return bool
|
|
function ImGui.BeginPopupContextItem(str_id, popup_flags)
|
|
if popup_flags == nil then popup_flags = 0 end
|
|
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if window.SkipItems then
|
|
return false
|
|
end
|
|
local id
|
|
if str_id then id = window:GetID(str_id) else id = g.LastItemData.ID end -- If user hasn't passed an ID, we can use the LastItem ID. Using LastItem ID as a Popup ID won't conflict!
|
|
IM_ASSERT(id ~= 0) -- You cannot pass a nil str_id if the last item has no identifier (e.g. a Text() item)
|
|
if ImGui.IsPopupOpenRequestForItem(popup_flags, g.LastItemData.ID) then
|
|
ImGui.OpenPopupEx(id, popup_flags)
|
|
end
|
|
return ImGui.BeginPopupEx(id, bit32.bor(ImGuiWindowFlags.AlwaysAutoResize, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoSavedSettings))
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] KEYBOARD/GAMEPAD NAVIGATION
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- FIXME-NAV: The existence of SetNavID vs SetFocusID vs FocusWindow() needs to be clarified/reworked.
|
|
-- In our terminology those should be interchangeable, yet right now this is super confusing.
|
|
-- Those two functions are merely a legacy artifact, so at minimum naming should be clarified.
|
|
|
|
--- @param id ImGuiID
|
|
function ImGui.PushFocusScope(id)
|
|
local g = GImGui
|
|
local data = ImGuiFocusScopeData()
|
|
data.ID = id
|
|
data.WindowID = g.CurrentWindow.ID
|
|
g.FocusScopeStack:push_back(data)
|
|
g.CurrentFocusScopeId = id
|
|
end
|
|
|
|
function ImGui.PopFocusScope()
|
|
local g = GImGui
|
|
-- TODO: IM_ASSERT_USER_ERROR_RET(g.FocusScopeStack.Size > g.StackSizesInBeginForCurrentWindow.SizeOfFocusScopeStack, "Calling PopFocusScope() too many times!")
|
|
g.FocusScopeStack:pop_back()
|
|
if g.FocusScopeStack.Size > 0 then
|
|
g.CurrentFocusScopeId = g.FocusScopeStack:back().ID
|
|
else
|
|
g.CurrentFocusScopeId = 0
|
|
end
|
|
end
|
|
|
|
--- @param focus_scope_id ImGuiID
|
|
function ImGui.IsInNavFocusRoute(focus_scope_id)
|
|
local g = GImGui
|
|
if g.NavFocusScopeId == focus_scope_id then
|
|
return true
|
|
end
|
|
for _, focus_scope in g.NavFocusRoute:iter() do
|
|
if focus_scope.ID == focus_scope_id then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- @param focus_scope_id ImGuiID
|
|
function ImGui.SetNavFocusScope(focus_scope_id)
|
|
local g = GImGui
|
|
g.NavFocusScopeId = focus_scope_id
|
|
g.NavFocusRoute:resize(0) -- Invalidate
|
|
if focus_scope_id == 0 then
|
|
return
|
|
end
|
|
IM_ASSERT(g.NavWindow ~= nil)
|
|
|
|
-- Store current path (in reverse order)
|
|
if focus_scope_id == g.CurrentFocusScopeId then
|
|
-- Top of focus stack contains local focus scopes inside current window
|
|
for n = g.FocusScopeStack.Size, 1, -1 do
|
|
local data = g.FocusScopeStack.Data[n]
|
|
if data.WindowID ~= g.CurrentWindow.ID then
|
|
break
|
|
end
|
|
g.NavFocusRoute:push_back(data)
|
|
end
|
|
elseif focus_scope_id == g.NavWindow.NavRootFocusScopeId then
|
|
local data = ImGuiFocusScopeData()
|
|
data.ID = focus_scope_id
|
|
data.WindowID = g.NavWindow.ID
|
|
g.NavFocusRoute:push_back(data)
|
|
else
|
|
return
|
|
end
|
|
|
|
-- Then follow on manually set ParentWindowForFocusRoute field (#6798)
|
|
local window = g.NavWindow.ParentWindowForFocusRoute
|
|
while window ~= nil do
|
|
local data = ImGuiFocusScopeData()
|
|
data.ID = window.NavRootFocusScopeId
|
|
data.WindowID = window.ID
|
|
g.NavFocusRoute:push_back(data)
|
|
window = window.ParentWindowForFocusRoute
|
|
end
|
|
IM_ASSERT(g.NavFocusRoute.Size < 100) -- Maximum depth is technically 251 as per CalcRoutingScore(): 254 - 3
|
|
end
|
|
|
|
function ImGui.SetItemDefaultFocus()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if not window.Appearing then
|
|
return
|
|
end
|
|
if g.NavWindow ~= window.RootWindowForNav or (not g.NavInitRequest and g.NavInitResult.ID == 0) or g.NavLayer ~= window.DC.NavLayerCurrent then
|
|
return
|
|
end
|
|
|
|
g.NavInitRequest = false
|
|
ImGui.NavApplyItemToResult(g.NavInitResult)
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
|
|
-- Scroll could be done in NavInitRequestApplyResult() via an opt-in flag (we however don't want regular init requests to scroll)
|
|
if not window.ClipRect:Contains(g.LastItemData.Rect) then
|
|
ImGui.ScrollToRectEx(window, g.LastItemData.Rect, ImGuiScrollFlags.None)
|
|
end
|
|
end
|
|
|
|
-- (was called NavRestoreHighlightAfterMove() before 1.91.4)
|
|
function ImGui.SetNavCursorVisibleAfterMove()
|
|
local g = GImGui
|
|
if g.NavWindow and (bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) ~= 0) then
|
|
g.NavCursorVisible = false
|
|
elseif g.NavInputSource == ImGuiInputSource.Keyboard and (bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) == 0) then
|
|
g.NavCursorVisible = false
|
|
elseif g.NavInputSource == ImGuiInputSource.Gamepad and (bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) == 0) then
|
|
g.NavCursorVisible = false
|
|
elseif g.IO.ConfigNavCursorVisibleAuto then
|
|
g.NavCursorVisible = true
|
|
end
|
|
g.NavHighlightItemUnderNav = true
|
|
g.NavMousePosDirty = true
|
|
end
|
|
|
|
--- @param window? ImGuiWindow
|
|
function ImGui.SetNavWindow(window)
|
|
local g = GImGui
|
|
if g.NavWindow ~= window then
|
|
-- IMGUI_DEBUG_LOG_FOCUS("[focus] SetNavWindow(\"%s\")", window and window.Name or "<NULL>")
|
|
g.NavWindow = window
|
|
g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid
|
|
end
|
|
g.NavInitRequest = false
|
|
g.NavMoveSubmitted = false
|
|
g.NavMoveScoringItems = false
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
|
|
--- @param axis ImGuiAxis
|
|
function ImGui.NavClearPreferredPosForAxis(axis)
|
|
local g = GImGui
|
|
g.NavWindow.RootWindowForNav.NavPreferredScoringPosRel[g.NavLayer][axis] = FLT_MAX
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
--- @param nav_layer ImGuiNavLayer
|
|
--- @param focus_scope_id ImGuiID
|
|
--- @param rect_rel ImRect
|
|
function ImGui.SetNavID(id, nav_layer, focus_scope_id, rect_rel)
|
|
local g = GImGui
|
|
IM_ASSERT(g.NavWindow ~= nil)
|
|
IM_ASSERT(nav_layer == ImGuiNavLayer.Main or nav_layer == ImGuiNavLayer.Menu)
|
|
g.NavId = id
|
|
g.NavLayer = nav_layer
|
|
ImGui.SetNavFocusScope(focus_scope_id)
|
|
g.NavWindow.NavLastIds[nav_layer] = id
|
|
ImRect_Copy(g.NavWindow.NavRectRel[nav_layer], rect_rel)
|
|
|
|
-- Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it)
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.X)
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.Y)
|
|
end
|
|
|
|
-- FIXME: This could be replaced by updating a frame number in each window when (window == NavWindow) and (NavLayer == 0).
|
|
-- This way we could find the last focused window among our children. It would be much less confusing this way?
|
|
--- @param nav_window ImGuiWindow
|
|
function ImGui.NavSaveLastChildNavWindowIntoParent(nav_window)
|
|
local parent = nav_window
|
|
while parent and parent.RootWindow ~= parent and bit32.band(parent.Flags, bit32.bor(ImGuiWindowFlags.Popup, ImGuiWindowFlags.ChildMenu)) == 0 do
|
|
parent = parent.ParentWindow
|
|
end
|
|
if parent and parent ~= nav_window then
|
|
parent.NavLastChildNavWindow = nav_window
|
|
end
|
|
end
|
|
|
|
-- Restore the last focused child.
|
|
-- Call when we are expected to land on the Main Layer (0) after FocusWindow()
|
|
--- @param window ImGuiWindow
|
|
function ImGui.NavRestoreLastChildNavWindow(window)
|
|
if window.NavLastChildNavWindow and window.NavLastChildNavWindow.WasActive then
|
|
return window.NavLastChildNavWindow
|
|
end
|
|
return window
|
|
end
|
|
|
|
--- @param layer ImGuiNavLayer
|
|
function ImGui.NavRestoreLayer(layer)
|
|
local g = GImGui
|
|
if layer == ImGuiNavLayer.Main then
|
|
local prev_nav_window = g.NavWindow
|
|
g.NavWindow = ImGui.NavRestoreLastChildNavWindow(g.NavWindow) -- FIXME-NAV: Should clear ongoing nav requests?
|
|
g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid
|
|
-- if (prev_nav_window) IMGUI_DEBUG_LOG_FOCUS("[focus] NavRestoreLayer: from \"%s\" to SetNavWindow(\"%s\")\n", prev_nav_window->Name, g.NavWindow->Name)
|
|
end
|
|
local window = g.NavWindow
|
|
if window.NavLastIds[layer] ~= 0 then
|
|
ImGui.SetNavID(window.NavLastIds[layer], layer, 0, window.NavRectRel[layer])
|
|
else
|
|
g.NavLayer = layer
|
|
ImGui.NavInitWindow(window, true)
|
|
end
|
|
end
|
|
|
|
--- @param id ImGuiID
|
|
function ImGui.NavHighlightActivated(id)
|
|
local g = GImGui
|
|
g.NavHighlightActivatedId = id
|
|
g.NavHighlightActivatedTimer = NAV_ACTIVATE_HIGHLIGHT_TIMER
|
|
end
|
|
|
|
function ImGui.NavUpdateAnyRequestFlag()
|
|
local g = GImGui
|
|
g.NavAnyRequest = g.NavMoveScoringItems or g.NavInitRequest or (IMGUI_DEBUG_NAV_SCORING and g.NavWindow ~= nil)
|
|
if g.NavAnyRequest then
|
|
IM_ASSERT(g.NavWindow ~= nil)
|
|
end
|
|
end
|
|
|
|
-- This needs to be called before we submit any widget (aka in or before Begin)
|
|
--- @param window ImGuiWindow
|
|
--- @param force_reinit bool
|
|
function ImGui.NavInitWindow(window, force_reinit)
|
|
-- FIXME: ChildWindow test here is wrong for docking
|
|
local g = GImGui
|
|
IM_ASSERT(window == g.NavWindow)
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoNavInputs) ~= 0 then
|
|
g.NavId = 0
|
|
ImGui.SetNavFocusScope(window.NavRootFocusScopeId)
|
|
return
|
|
end
|
|
|
|
local init_for_nav = false
|
|
if window == window.RootWindow or bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0 or window.NavLastIds[0] == 0 or force_reinit then
|
|
init_for_nav = true
|
|
end
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from NavInitWindow(), init_for_nav=%d, window=\"%s\", layer=%d", init_for_nav, window->Name, g.NavLayer)
|
|
if init_for_nav then
|
|
ImGui.SetNavID(0, g.NavLayer, window.NavRootFocusScopeId, ImRect())
|
|
g.NavInitRequest = true
|
|
g.NavInitRequestFromMove = false
|
|
g.NavInitResult.ID = 0
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
else
|
|
g.NavId = window.NavLastIds[0]
|
|
ImGui.SetNavFocusScope(window.NavRootFocusScopeId)
|
|
end
|
|
end
|
|
|
|
-- Positioning logic altered slightly for remote activation: for Popup we want to use item rect, for Tooltip we leave things alone. (#9138)
|
|
-- When calling for ImGuiWindowFlags.Popup we use LastItemData.
|
|
--- @param window_type ImGuiWindowFlags
|
|
--- @return ImGuiInputSource
|
|
function ImGui.NavCalcPreferredRefPosSource(window_type)
|
|
local g = GImGui
|
|
local window = g.NavWindow
|
|
|
|
local activated_shortcut = g.ActiveId ~= 0 and g.ActiveIdFromShortcut and g.ActiveId == g.LastItemData.ID
|
|
if (bit32.band(window_type, ImGuiWindowFlags.Popup) ~= 0) and activated_shortcut then
|
|
return ImGuiInputSource.Keyboard
|
|
end
|
|
|
|
if not g.NavCursorVisible or not g.NavHighlightItemUnderNav or not window then
|
|
return ImGuiInputSource.Mouse
|
|
else
|
|
return ImGuiInputSource.Keyboard -- or Nav in general
|
|
end
|
|
end
|
|
|
|
--- @param window_type ImGuiWindowFlags
|
|
--- @return ImVec2
|
|
--- @nodiscard
|
|
function ImGui.NavCalcPreferredRefPos(window_type)
|
|
local g = GImGui
|
|
local window = g.NavWindow
|
|
local source = ImGui.NavCalcPreferredRefPosSource(window_type)
|
|
|
|
if source == ImGuiInputSource.Mouse then
|
|
-- Mouse (we need a fallback in case the mouse becomes invalid after being used)
|
|
-- The +1.0f offset when stored by OpenPopupEx() allows reopening this or another popup (same or another mouse button) while not moving the mouse, it is pretty standard.
|
|
-- In theory we could move that +1.0f offset in OpenPopupEx()
|
|
local p = ImGui.IsMousePosValid(g.IO.MousePos) and g.IO.MousePos or g.MouseLastValidPos
|
|
return ImVec2(p.x + 1.0, p.y)
|
|
else
|
|
-- When navigation is active and mouse is disabled, pick a position around the bottom left of the currently navigated item
|
|
local activated_shortcut = g.ActiveId ~= 0 and g.ActiveIdFromShortcut and g.ActiveId == g.LastItemData.ID
|
|
|
|
local ref_rect = ImRect()
|
|
if activated_shortcut and (bit32.band(window_type, ImGuiWindowFlags.Popup) ~= 0) then
|
|
ImRect_Copy(ref_rect, g.LastItemData.NavRect)
|
|
elseif window ~= nil then
|
|
ImRect_Copy(ref_rect, ImGui.WindowRectRelToAbs(window, window.NavRectRel[g.NavLayer]))
|
|
end
|
|
|
|
-- Take account of upcoming scrolling (maybe set mouse pos should be done in EndFrame?)
|
|
if window ~= nil and window.LastFrameActive ~= g.FrameCount and (window.ScrollTarget.x ~= FLT_MAX or window.ScrollTarget.y ~= FLT_MAX) then
|
|
local next_scroll = CalcNextScrollFromScrollTargetAndClamp(window)
|
|
ref_rect:Translate(window.Scroll - next_scroll)
|
|
end
|
|
|
|
local pos = ImVec2(ref_rect.Min.x + ImMin(g.Style.FramePadding.x * 4, ref_rect:GetWidth()), ref_rect.Max.y - ImMin(g.Style.FramePadding.y, ref_rect:GetHeight()))
|
|
if window ~= nil then
|
|
local viewport = window.Viewport
|
|
if viewport then
|
|
pos = ImClamp(pos, viewport.Pos, viewport.Pos + viewport.Size)
|
|
end
|
|
end
|
|
|
|
return ImTrunc(pos) -- ImTrunc() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta.
|
|
end
|
|
end
|
|
|
|
--- @param axis ImGuiAxis
|
|
--- @return float
|
|
function ImGui.GetNavTweakPressedAmount(axis)
|
|
local g = GImGui
|
|
local repeat_delay, repeat_rate = ImGui.GetTypematicRepeatRate(ImGuiInputFlags.RepeatRateNavTweak)
|
|
|
|
local key_less, key_more
|
|
if g.NavInputSource == ImGuiInputSource.Gamepad then
|
|
key_less = (axis == ImGuiAxis.X) and ImGuiKey.GamepadDpadLeft or ImGuiKey.GamepadDpadUp
|
|
key_more = (axis == ImGuiAxis.X) and ImGuiKey.GamepadDpadRight or ImGuiKey.GamepadDpadDown
|
|
else
|
|
key_less = (axis == ImGuiAxis.X) and ImGuiKey.LeftArrow or ImGuiKey.UpArrow
|
|
key_more = (axis == ImGuiAxis.X) and ImGuiKey.RightArrow or ImGuiKey.DownArrow
|
|
end
|
|
|
|
local amount = ImGui.GetKeyPressedAmount(key_more, repeat_delay, repeat_rate) - ImGui.GetKeyPressedAmount(key_less, repeat_delay, repeat_rate)
|
|
|
|
if amount ~= 0.0 and ImGui.IsKeyDown(key_less) and ImGui.IsKeyDown(key_more) then -- Cancel when opposite directions are held, regardless of repeat phase
|
|
amount = 0.0
|
|
end
|
|
|
|
return amount
|
|
end
|
|
|
|
do
|
|
|
|
local nav_gamepad_keys_to_change_source = { ImGuiKey.GamepadFaceRight, ImGuiKey.GamepadFaceLeft, ImGuiKey.GamepadFaceUp, ImGuiKey.GamepadFaceDown, ImGuiKey.GamepadDpadRight, ImGuiKey.GamepadDpadLeft, ImGuiKey.GamepadDpadUp, ImGuiKey.GamepadDpadDown }
|
|
local nav_keyboard_keys_to_change_source = { ImGuiKey.Space, ImGuiKey.Enter, ImGuiKey.Escape, ImGuiKey.RightArrow, ImGuiKey.LeftArrow, ImGuiKey.UpArrow, ImGuiKey.DownArrow }
|
|
|
|
--- @param focus_change_dir int
|
|
function ImGui.NavUpdateWindowingTarget(focus_change_dir)
|
|
local g = GImGui
|
|
IM_ASSERT(g.NavWindowingTarget)
|
|
if bit32.band(g.NavWindowingTarget.Flags, ImGuiWindowFlags.Modal) ~= 0 then
|
|
return
|
|
end
|
|
|
|
local i_current = ImGui.FindWindowFocusIndex(g.NavWindowingTarget)
|
|
local window_target = ImGui.FindWindowNavFocusable(i_current + focus_change_dir, 0, focus_change_dir)
|
|
if window_target == nil then
|
|
window_target = ImGui.FindWindowNavFocusable((focus_change_dir < 0) and g.WindowsFocusOrder.Size or 1, i_current, focus_change_dir)
|
|
end
|
|
if window_target then -- Don't reset windowing target if there's a single window in the list
|
|
g.NavWindowingTarget = window_target
|
|
g.NavWindowingTargetAnim = window_target
|
|
ImVec2_CopyV(g.NavWindowingAccumDeltaPos, 0.0, 0.0)
|
|
ImVec2_CopyV(g.NavWindowingAccumDeltaSize, 0.0, 0.0)
|
|
end
|
|
g.NavWindowingToggleLayer = false
|
|
end
|
|
|
|
-- Apply focus and close overlay
|
|
--- @param apply_focus_window ImGuiWindow
|
|
function ImGui.NavUpdateWindowingApplyFocus(apply_focus_window)
|
|
-- FIXME: Many actions here could be part of a higher-level/reused function. Why aren't they in FocusWindow() ?
|
|
-- Investigate for each of them: ClearActiveID(), NavRestoreHighlightAfterMove(), NavRestoreLastChildNavWindow(), ClosePopupsOverWindow(), NavInitWindow()
|
|
local g = GImGui
|
|
if g.NavWindow == nil or apply_focus_window ~= g.NavWindow.RootWindow then
|
|
local previous_viewport = g.NavWindow and g.NavWindow.Viewport or nil
|
|
ImGui.ClearActiveID()
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
ImGui.ClosePopupsOverWindow(apply_focus_window, false)
|
|
ImGui.FocusWindow(apply_focus_window, ImGuiFocusRequestFlags.RestoreFocusedChild)
|
|
IM_ASSERT(g.NavWindow ~= nil)
|
|
apply_focus_window = g.NavWindow
|
|
if apply_focus_window.NavLastIds[ImGuiNavLayer.Main] == 0 then -- FIXME: This is the equivalent of the 'if (g.NavId == 0) { NavInitWindow() }' in DockNodeUpdateTabBar().
|
|
ImGui.NavInitWindow(apply_focus_window, false)
|
|
end
|
|
|
|
-- If the window has ONLY a menu layer (no main layer), select it directly
|
|
-- Use NavLayersActiveMaskNext since windows didn't have a chance to be Begin()-ed on this frame,
|
|
-- so Ctrl+Tab where the keys are only held for 1 frame will be able to use correct layers mask since
|
|
-- the target window as already been previewed once.
|
|
-- FIXME-NAV: This should be done in NavInit.. or in FocusWindow... However in both of those cases,
|
|
-- we won't have a guarantee that windows has been visible before and therefore NavLayersActiveMask*
|
|
-- won't be valid.
|
|
if apply_focus_window.DC.NavLayersActiveMaskNext == bit32.lshift(1, ImGuiNavLayer.Menu) then
|
|
g.NavLayer = ImGuiNavLayer.Menu
|
|
end
|
|
|
|
-- Request OS level focus
|
|
if apply_focus_window.Viewport ~= previous_viewport and g.PlatformIO.Platform_SetWindowFocus then
|
|
g.PlatformIO.Platform_SetWindowFocus(apply_focus_window.Viewport)
|
|
end
|
|
end
|
|
g.NavWindowingTarget = nil
|
|
end
|
|
|
|
-- Windowing management mode
|
|
-- Keyboard: Ctrl+Tab (change focus/move/resize), Alt (toggle menu layer)
|
|
-- Gamepad: Hold Menu/Square (change focus/move/resize), Tap Menu/Square (toggle menu layer)
|
|
function ImGui.NavUpdateWindowing()
|
|
local g = GImGui
|
|
local io = g.IO
|
|
|
|
local apply_focus_window = nil
|
|
local apply_toggle_layer = false
|
|
|
|
local modal_window = ImGui.GetTopMostPopupModal()
|
|
local allow_windowing = (modal_window == nil) -- FIXME: This prevent Ctrl+Tab from being usable with windows that are inside the Begin-stack of that modal.
|
|
if not allow_windowing then
|
|
g.NavWindowingTarget = nil
|
|
end
|
|
|
|
-- Fade out
|
|
if g.NavWindowingTargetAnim and g.NavWindowingTarget == nil then
|
|
g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha - io.DeltaTime * 10.0, 0.0)
|
|
if g.DimBgRatio <= 0.0 and g.NavWindowingHighlightAlpha <= 0.0 then
|
|
g.NavWindowingTargetAnim = nil
|
|
end
|
|
end
|
|
|
|
-- Start Ctrl+Tab or Square+L/R window selection
|
|
-- (g.ConfigNavWindowingKeyNext/g.ConfigNavWindowingKeyPrev defaults are ImGuiMod_Ctrl|ImGuiKey.Tab and ImGuiMod_Ctrl|ImGuiMod_Shift|ImGuiKey.Tab)
|
|
local owner_id = ImHashStr("##NavUpdateWindowing")
|
|
local nav_gamepad_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) ~= 0 and bit32.band(io.BackendFlags, ImGuiBackendFlags.HasGamepad) ~= 0
|
|
local nav_keyboard_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0
|
|
local keyboard_next_window = allow_windowing and g.ConfigNavWindowingKeyNext ~= 0 and ImGui.Shortcut(g.ConfigNavWindowingKeyNext, bit32.bor(ImGuiInputFlags.Repeat, ImGuiInputFlags.RouteAlways), owner_id)
|
|
local keyboard_prev_window = allow_windowing and g.ConfigNavWindowingKeyPrev ~= 0 and ImGui.Shortcut(g.ConfigNavWindowingKeyPrev, bit32.bor(ImGuiInputFlags.Repeat, ImGuiInputFlags.RouteAlways), owner_id)
|
|
local start_toggling_with_gamepad = nav_gamepad_active and g.NavWindowingTarget == nil and ImGui.Shortcut(ImGuiKey.NavGamepadMenu, ImGuiInputFlags.RouteAlways, owner_id)
|
|
local start_windowing_with_gamepad = allow_windowing and start_toggling_with_gamepad
|
|
local start_windowing_with_keyboard = allow_windowing and g.NavWindowingTarget == nil and (keyboard_next_window or keyboard_prev_window) -- Note: enabled even without NavEnableKeyboard!
|
|
local just_started_windowing_from_null_focus = false
|
|
if start_toggling_with_gamepad then
|
|
g.NavWindowingToggleLayer = true -- Gamepad starts toggling layer
|
|
g.NavWindowingToggleKey = ImGuiKey.NavGamepadMenu
|
|
g.NavWindowingInputSource = ImGuiInputSource.Gamepad
|
|
g.NavInputSource = ImGuiInputSource.Gamepad
|
|
end
|
|
if start_windowing_with_gamepad or start_windowing_with_keyboard then
|
|
local window = (g.NavWindow and ImGui.IsWindowNavFocusable(g.NavWindow)) and g.NavWindow or ImGui.FindWindowNavFocusable(g.WindowsFocusOrder.Size, 0, -1)
|
|
if window then
|
|
if start_windowing_with_keyboard or g.ConfigNavWindowingWithGamepad then
|
|
g.NavWindowingTarget = window.RootWindow
|
|
g.NavWindowingTargetAnim = window.RootWindow -- Current location
|
|
end
|
|
g.NavWindowingTimer = 0.0
|
|
g.NavWindowingHighlightAlpha = 0.0
|
|
ImVec2_CopyV(g.NavWindowingAccumDeltaPos, 0.0, 0.0)
|
|
ImVec2_CopyV(g.NavWindowingAccumDeltaSize, 0.0, 0.0)
|
|
g.NavWindowingInputSource = start_windowing_with_keyboard and ImGuiInputSource.Keyboard or ImGuiInputSource.Gamepad
|
|
g.NavInputSource = g.NavWindowingInputSource
|
|
if g.NavWindow == nil then
|
|
just_started_windowing_from_null_focus = true
|
|
end
|
|
|
|
-- Manually register ownership of our mods. Using a global route in the Shortcut() calls instead would probably be correct but may have more side-effects.
|
|
if keyboard_next_window or keyboard_prev_window then
|
|
ImGui.SetKeyOwnersForKeyChord(bit32.band(bit32.bor(g.ConfigNavWindowingKeyNext, g.ConfigNavWindowingKeyPrev), ImGuiMod_Mask_), owner_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Gamepad update
|
|
if (g.NavWindowingTarget or g.NavWindowingToggleLayer) and g.NavWindowingInputSource == ImGuiInputSource.Gamepad then
|
|
if g.NavWindowingTarget ~= nil then
|
|
-- Highlight only appears after a brief time holding the button, so that a fast tap on ImGuiKey.NavGamepadMenu (to toggle NavLayer) doesn't add visual noise
|
|
-- However inputs are accepted immediately, so you press ImGuiKey.NavGamepadMenu + L1/R1 fast.
|
|
g.NavWindowingTimer = g.NavWindowingTimer + io.DeltaTime
|
|
g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05))
|
|
|
|
-- Select window to focus
|
|
local focus_change_dir = (ImGui.IsKeyPressedEx(ImGuiKey.GamepadL1, ImGuiInputFlags.None, ImGuiKeyOwner_Any) and 1 or 0) - (ImGui.IsKeyPressedEx(ImGuiKey.GamepadR1, ImGuiInputFlags.None, ImGuiKeyOwner_Any) and 1 or 0)
|
|
if focus_change_dir ~= 0 and not just_started_windowing_from_null_focus then
|
|
ImGui.NavUpdateWindowingTarget(focus_change_dir)
|
|
g.NavWindowingHighlightAlpha = 1.0
|
|
end
|
|
end
|
|
|
|
-- Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered top-most)
|
|
if not ImGui.IsKeyDown(ImGuiKey.NavGamepadMenu) then
|
|
g.NavWindowingToggleLayer = g.NavWindowingToggleLayer and (g.NavWindowingHighlightAlpha < 1.0) -- Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore.
|
|
if g.NavWindowingToggleLayer and g.NavWindow then
|
|
apply_toggle_layer = true
|
|
elseif not g.NavWindowingToggleLayer then
|
|
apply_focus_window = g.NavWindowingTarget
|
|
end
|
|
g.NavWindowingTarget = nil
|
|
g.NavWindowingToggleLayer = false
|
|
end
|
|
end
|
|
|
|
-- Keyboard: Focus
|
|
if g.NavWindowingTarget and g.NavWindowingInputSource == ImGuiInputSource.Keyboard then
|
|
-- Visuals only appears after a brief time after pressing TAB the first time, so that a fast Ctrl+Tab doesn't add visual noise
|
|
local shared_mods = bit32.band(
|
|
bit32.band((g.ConfigNavWindowingKeyNext ~= 0) and g.ConfigNavWindowingKeyNext or ImGuiMod_Mask_, (g.ConfigNavWindowingKeyPrev ~= 0) and g.ConfigNavWindowingKeyPrev or ImGuiMod_Mask_),
|
|
ImGuiMod_Mask_)
|
|
IM_ASSERT(shared_mods ~= 0) -- Next/Prev shortcut currently needs a shared modifier to "hold", otherwise Prev actions would keep cycling between two windows.
|
|
g.NavWindowingTimer = g.NavWindowingTimer + io.DeltaTime
|
|
g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05)) -- 1.0f
|
|
if (keyboard_next_window or keyboard_prev_window) and not just_started_windowing_from_null_focus then
|
|
ImGui.NavUpdateWindowingTarget(keyboard_next_window and -1 or 1)
|
|
elseif bit32.band(io.KeyMods, shared_mods) ~= shared_mods then
|
|
apply_focus_window = g.NavWindowingTarget
|
|
end
|
|
end
|
|
|
|
-- Keyboard: Press and Release Alt to toggle menu layer
|
|
local windowing_toggle_keys = { ImGuiKey.LeftAlt, ImGuiKey.RightAlt }
|
|
local windowing_toggle_layer_start = false
|
|
if g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) == 0 then
|
|
for _, windowing_toggle_key in ipairs(windowing_toggle_keys) do
|
|
if nav_keyboard_active and ImGui.IsKeyPressedEx(windowing_toggle_key, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner) then
|
|
windowing_toggle_layer_start = true
|
|
g.NavWindowingToggleLayer = true
|
|
g.NavWindowingToggleKey = windowing_toggle_key
|
|
g.NavWindowingInputSource = ImGuiInputSource.Keyboard
|
|
g.NavInputSource = ImGuiInputSource.Keyboard
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if g.NavWindowingToggleLayer and g.NavWindowingInputSource == ImGuiInputSource.Keyboard then
|
|
-- We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370)
|
|
-- We cancel toggling nav layer when other modifiers are pressed. (See #4439)
|
|
-- - AltGR is Alt+Ctrl on some layout but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl).
|
|
-- We cancel toggling nav layer if an owner has claimed the key.
|
|
if io.InputQueueCharacters.Size > 0 or io.KeyCtrl or io.KeyShift or io.KeySuper then
|
|
g.NavWindowingToggleLayer = false
|
|
elseif windowing_toggle_layer_start == false and g.LastKeyboardKeyPressTime == g.Time then
|
|
g.NavWindowingToggleLayer = false
|
|
elseif ImGui.TestKeyOwner(g.NavWindowingToggleKey, ImGuiKeyOwner_NoOwner) == false or ImGui.TestKeyOwner(ImGuiMod_Alt, ImGuiKeyOwner_NoOwner) == false then
|
|
g.NavWindowingToggleLayer = false
|
|
end
|
|
|
|
-- Apply layer toggle on Alt release
|
|
-- Important: as before version <18314 we lacked an explicit IO event for focus gain/loss, we also compare mouse validity to detect old backends clearing mouse pos on focus loss.
|
|
if ImGui.IsKeyReleased(g.NavWindowingToggleKey) and g.NavWindowingToggleLayer then
|
|
if g.ActiveId == 0 or g.ActiveIdAllowOverlap then
|
|
if ImGui.IsMousePosValid(io.MousePos) == ImGui.IsMousePosValid(io.MousePosPrev) then
|
|
apply_toggle_layer = true
|
|
end
|
|
end
|
|
end
|
|
if not ImGui.IsKeyDown(g.NavWindowingToggleKey) then
|
|
g.NavWindowingToggleLayer = false
|
|
end
|
|
end
|
|
|
|
-- Move window
|
|
if g.NavWindowingTarget and bit32.band(g.NavWindowingTarget.Flags, ImGuiWindowFlags.NoMove) == 0 then
|
|
local nav_move_dir
|
|
if g.NavInputSource == ImGuiInputSource.Keyboard and not io.KeyShift then
|
|
nav_move_dir = ImGui.GetKeyMagnitude2d(ImGuiKey.LeftArrow, ImGuiKey.RightArrow, ImGuiKey.UpArrow, ImGuiKey.DownArrow)
|
|
end
|
|
if g.NavInputSource == ImGuiInputSource.Gamepad then
|
|
nav_move_dir = ImGui.GetKeyMagnitude2d(ImGuiKey.GamepadLStickLeft, ImGuiKey.GamepadLStickRight, ImGuiKey.GamepadLStickUp, ImGuiKey.GamepadLStickDown)
|
|
end
|
|
if nav_move_dir and (nav_move_dir.x ~= 0.0 or nav_move_dir.y ~= 0.0) then
|
|
local NAV_MOVE_SPEED = 800.0
|
|
local move_step = NAV_MOVE_SPEED * io.DeltaTime * ImGui.GetScale()
|
|
ImVec2_Copy(g.NavWindowingAccumDeltaPos, g.NavWindowingAccumDeltaPos + nav_move_dir * move_step)
|
|
g.NavHighlightItemUnderNav = true
|
|
local accum_floored = ImTrunc(g.NavWindowingAccumDeltaPos)
|
|
if accum_floored.x ~= 0.0 or accum_floored.y ~= 0.0 then
|
|
local moving_window = g.NavWindowingTarget.RootWindowDockTree
|
|
ImGui.SetWindowPos(moving_window, moving_window.Pos + accum_floored, ImGuiCond.Always)
|
|
ImVec2_Copy(g.NavWindowingAccumDeltaPos, g.NavWindowingAccumDeltaPos - accum_floored)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Apply final focus
|
|
if apply_focus_window then
|
|
ImGui.NavUpdateWindowingApplyFocus(apply_focus_window)
|
|
end
|
|
|
|
-- Apply menu/layer toggle
|
|
if apply_toggle_layer and g.NavWindow then
|
|
ImGui.ClearActiveID()
|
|
|
|
-- Move to parent menu if necessary
|
|
local new_nav_window = g.NavWindow
|
|
while new_nav_window.ParentWindow
|
|
and bit32.band(new_nav_window.DC.NavLayersActiveMask, bit32.lshift(1, ImGuiNavLayer.Menu)) == 0
|
|
and bit32.band(new_nav_window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0
|
|
and bit32.band(new_nav_window.Flags, bit32.bor(ImGuiWindowFlags.Popup, ImGuiWindowFlags.ChildMenu)) == 0 do
|
|
new_nav_window = new_nav_window.ParentWindow
|
|
end
|
|
if new_nav_window ~= g.NavWindow then
|
|
local old_nav_window = g.NavWindow
|
|
ImGui.FocusWindow(new_nav_window)
|
|
new_nav_window.NavLastChildNavWindow = old_nav_window
|
|
end
|
|
|
|
-- Toggle layer
|
|
local new_nav_layer = (bit32.band(g.NavWindow.DC.NavLayersActiveMask, bit32.lshift(1, ImGuiNavLayer.Menu)) ~= 0) and bit32.bxor(g.NavLayer, 1) or ImGuiNavLayer.Main
|
|
if new_nav_layer ~= g.NavLayer then
|
|
-- Reinitialize navigation when entering menu bar with the Alt key (FIXME: could be a properly of the layer?)
|
|
local preserve_layer_1_nav_id = (new_nav_window.DockNodeAsHost ~= nil)
|
|
if new_nav_layer == ImGuiNavLayer.Menu and not preserve_layer_1_nav_id then
|
|
g.NavWindow.NavLastIds[new_nav_layer] = 0
|
|
end
|
|
ImGui.NavRestoreLayer(new_nav_layer)
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.NavUpdateContextMenuRequest()
|
|
local g = GImGui
|
|
local io = g.IO
|
|
g.NavOpenContextMenuItemId = 0
|
|
g.NavOpenContextMenuWindowId = 0
|
|
local nav_keyboard_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0
|
|
local nav_gamepad_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) ~= 0
|
|
if (not nav_keyboard_active and not nav_gamepad_active) or g.NavWindow == nil then
|
|
return
|
|
end
|
|
|
|
local request = false
|
|
request = request or (nav_keyboard_active and (ImGui.IsKeyReleased(ImGuiKey.Menu, ImGuiKeyOwner_NoOwner) or (ImGui.IsKeyPressedEx(ImGuiKey.F10, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner) and io.KeyMods == ImGuiMod_Shift)))
|
|
request = request or (nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.NavGamepadContextMenu, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner))
|
|
if not request then
|
|
return
|
|
end
|
|
|
|
g.NavOpenContextMenuItemId = g.NavId
|
|
g.NavOpenContextMenuWindowId = g.NavWindow.ID
|
|
|
|
-- Allow triggering for Begin()..BeginPopupContextItem(). A possible alternative would be to use g.NavLayer == ImGuiNavLayer.Menu.
|
|
if g.NavId == g.NavWindow:GetID("#CLOSE") or g.NavId == g.NavWindow:GetID("#COLLAPSE") then
|
|
g.NavOpenContextMenuItemId = g.NavWindow.MoveId
|
|
end
|
|
|
|
g.NavInputSource = ImGuiInputSource.Keyboard
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
end
|
|
|
|
-- FIXME: ScoringRect is not set
|
|
--- @param move_dir ImGuiDir
|
|
--- @param clip_dir ImGuiDir
|
|
--- @param move_flags ImGuiNavMoveFlags
|
|
--- @param scroll_flags ImGuiScrollFlags
|
|
function ImGui.NavMoveRequestSubmit(move_dir, clip_dir, move_flags, scroll_flags)
|
|
local g = GImGui
|
|
IM_ASSERT(g.NavWindow ~= nil)
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestSubmit: dir %c, window \"%s\"\n", "-WENS"[move_dir + 1], g.NavWindow->Name)
|
|
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.IsTabbing) ~= 0 then
|
|
move_flags = bit32.bor(move_flags, ImGuiNavMoveFlags.AllowCurrentNavId)
|
|
end
|
|
g.NavMoveSubmitted = true; g.NavMoveScoringItems = true
|
|
g.NavMoveDir = move_dir
|
|
g.NavMoveDirForDebug = move_dir
|
|
g.NavMoveClipDir = clip_dir
|
|
g.NavMoveFlags = move_flags
|
|
g.NavMoveScrollFlags = scroll_flags
|
|
g.NavMoveForwardToNextFrame = false
|
|
g.NavMoveKeyMods = (bit32.band(move_flags, ImGuiNavMoveFlags.FocusApi) ~= 0) and 0 or g.IO.KeyMods
|
|
g.NavMoveResultLocal:Clear()
|
|
g.NavMoveResultLocalVisible:Clear()
|
|
g.NavMoveResultOther:Clear()
|
|
g.NavTabbingCounter = 0
|
|
g.NavTabbingResultFirst:Clear()
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
|
|
-- Bias scoring rect ahead of scoring + update preferred pos (if missing) using source position
|
|
--- @param r ImRect
|
|
--- @param preferred_pos_rel ImVec2
|
|
--- @param move_dir ImGuiDir
|
|
--- @param move_flags ImGuiNavMoveFlags
|
|
function ImGui.NavBiasScoringRect(r, preferred_pos_rel, move_dir, move_flags)
|
|
-- Bias initial rect
|
|
local g = GImGui
|
|
local rel_to_abs_offset = g.NavWindow.DC.CursorStartPos
|
|
|
|
-- Initialize bias on departure if we don't have any. So mouse-click + arrow will record bias.
|
|
-- - We default to L/U bias, so moving down from a large source item into several columns will land on left-most column.
|
|
-- - But each successful move sets new bias on one axis, only cleared when using mouse.
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.Forwarded) == 0 then
|
|
if preferred_pos_rel.x == FLT_MAX then
|
|
preferred_pos_rel.x = ImMin(r.Min.x + 1.0, r.Max.x) - rel_to_abs_offset.x
|
|
end
|
|
if preferred_pos_rel.y == FLT_MAX then
|
|
preferred_pos_rel.y = r:GetCenter().y - rel_to_abs_offset.y
|
|
end
|
|
end
|
|
|
|
-- Apply general bias on the other axis
|
|
if (move_dir == ImGuiDir.Up or move_dir == ImGuiDir.Down) and preferred_pos_rel.x ~= FLT_MAX then
|
|
local new_x = preferred_pos_rel.x + rel_to_abs_offset.x
|
|
r.Min.x = new_x; r.Max.x = new_x
|
|
elseif (move_dir == ImGuiDir.Left or move_dir == ImGuiDir.Right) and preferred_pos_rel.y ~= FLT_MAX then
|
|
local new_y = preferred_pos_rel.y + rel_to_abs_offset.y
|
|
r.Min.y = new_y; r.Max.y = new_y
|
|
end
|
|
end
|
|
|
|
-- Handle PageUp/PageDown/Home/End keys
|
|
-- Called from NavUpdateCreateMoveRequest() which will use our output to create a move request
|
|
-- FIXME-NAV: This doesn't work properly with NavFlattened siblings as we use NavWindow rectangle for reference
|
|
-- FIXME-NAV: how to get Home/End to aim at the beginning/end of a 2D grid?
|
|
--- @return float
|
|
function ImGui.NavUpdatePageUpPageDown()
|
|
local g = GImGui
|
|
local window = g.NavWindow
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoNavInputs) ~= 0 or g.NavWindowingTarget ~= nil then
|
|
return 0.0
|
|
end
|
|
|
|
local page_up_held = ImGui.IsKeyDown(ImGuiKey.PageUp, ImGuiKeyOwner_NoOwner)
|
|
local page_down_held = ImGui.IsKeyDown(ImGuiKey.PageDown, ImGuiKeyOwner_NoOwner)
|
|
local home_pressed = ImGui.IsKeyPressedEx(ImGuiKey.Home, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner)
|
|
local end_pressed = ImGui.IsKeyPressedEx(ImGuiKey.End, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner)
|
|
if page_up_held == page_down_held and home_pressed == end_pressed then -- Proceed if either (not both) are pressed, otherwise early out
|
|
return 0.0
|
|
end
|
|
|
|
if g.NavLayer ~= ImGuiNavLayer.Main then
|
|
ImGui.NavRestoreLayer(ImGuiNavLayer.Main)
|
|
end
|
|
|
|
if bit32.band(window.DC.NavLayersActiveMask, bit32.lshift(1, ImGuiNavLayer.Main)) == 0 and window.DC.NavWindowHasScrollY then
|
|
-- Fallback manual-scroll when window has no navigable item
|
|
if ImGui.IsKeyPressedEx(ImGuiKey.PageUp, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner) then
|
|
ImGui.SetScrollY(window, window.Scroll.y - window.InnerRect:GetHeight())
|
|
elseif ImGui.IsKeyPressedEx(ImGuiKey.PageDown, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner) then
|
|
ImGui.SetScrollY(window, window.Scroll.y + window.InnerRect:GetHeight())
|
|
elseif home_pressed then
|
|
ImGui.SetScrollY(window, 0.0)
|
|
elseif end_pressed then
|
|
ImGui.SetScrollY(window, window.ScrollMax.y)
|
|
end
|
|
else
|
|
local nav_rect_rel = window.NavRectRel[g.NavLayer]
|
|
local page_offset_y = ImMax(0.0, window.InnerRect:GetHeight() - window.FontRefSize * 1.0 + nav_rect_rel:GetHeight())
|
|
local nav_scoring_rect_offset_y = 0.0
|
|
if ImGui.IsKeyPressedEx(ImGuiKey.PageUp, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner) then
|
|
nav_scoring_rect_offset_y = -page_offset_y
|
|
g.NavMoveDir = ImGuiDir.Down -- Because our scoring rect is offset up, we request the down direction (so we can always land on the last item)
|
|
g.NavMoveClipDir = ImGuiDir.Up
|
|
g.NavMoveFlags = bit32.bor(ImGuiNavMoveFlags.AllowCurrentNavId, ImGuiNavMoveFlags.IsPageMove) -- ImGuiNavMoveFlags.AlsoScoreVisibleSet may be added later
|
|
elseif ImGui.IsKeyPressedEx(ImGuiKey.PageDown, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner) then
|
|
nav_scoring_rect_offset_y = page_offset_y
|
|
g.NavMoveDir = ImGuiDir.Up -- Because our scoring rect is offset down, we request the up direction (so we can always land on the last item)
|
|
g.NavMoveClipDir = ImGuiDir.Down
|
|
g.NavMoveFlags = bit32.bor(ImGuiNavMoveFlags.AllowCurrentNavId, ImGuiNavMoveFlags.IsPageMove) -- ImGuiNavMoveFlags.AlsoScoreVisibleSet may be added later
|
|
elseif home_pressed then
|
|
-- FIXME-NAV: handling of Home/End is assuming that the top/bottom most item will be visible with Scroll.y == 0/ScrollMax.y
|
|
-- Scrolling will be handled via the ImGuiNavMoveFlags.ScrollToEdgeY flag, we don't scroll immediately to avoid scrolling happening before nav result.
|
|
-- Preserve current horizontal position if we have any.
|
|
nav_rect_rel.Min.y = 0.0; nav_rect_rel.Max.y = 0.0
|
|
if nav_rect_rel:IsInverted() then
|
|
nav_rect_rel.Min.x = 0.0; nav_rect_rel.Max.x = 0.0
|
|
end
|
|
g.NavMoveDir = ImGuiDir.Down
|
|
g.NavMoveFlags = bit32.bor(ImGuiNavMoveFlags.AllowCurrentNavId, ImGuiNavMoveFlags.ScrollToEdgeY)
|
|
-- FIXME-NAV: MoveClipDir left to _None, intentional?
|
|
elseif end_pressed then
|
|
nav_rect_rel.Min.y = window.ContentSize.y; nav_rect_rel.Max.y = window.ContentSize.y
|
|
if nav_rect_rel:IsInverted() then
|
|
nav_rect_rel.Min.x = 0.0; nav_rect_rel.Max.x = 0.0
|
|
end
|
|
g.NavMoveDir = ImGuiDir.Up
|
|
g.NavMoveFlags = bit32.bor(ImGuiNavMoveFlags.AllowCurrentNavId, ImGuiNavMoveFlags.ScrollToEdgeY)
|
|
-- FIXME-NAV: MoveClipDir left to _None, intentional?
|
|
end
|
|
return nav_scoring_rect_offset_y
|
|
end
|
|
return 0.0
|
|
end
|
|
|
|
function ImGui.NavUpdateCreateMoveRequest()
|
|
local g = GImGui
|
|
local io = g.IO
|
|
local window = g.NavWindow
|
|
local nav_gamepad_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) ~= 0 and bit32.band(io.BackendFlags, ImGuiBackendFlags.HasGamepad) ~= 0
|
|
local nav_keyboard_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0
|
|
|
|
if g.NavMoveForwardToNextFrame and window ~= nil then
|
|
-- Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window)
|
|
-- (preserve most state, which were already set by the NavMoveRequestForward() function)
|
|
IM_ASSERT(g.NavMoveDir ~= ImGuiDir.None and g.NavMoveClipDir ~= ImGuiDir.None)
|
|
IM_ASSERT(bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.Forwarded) ~= 0)
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestForward %d\n", g.NavMoveDir)
|
|
else
|
|
-- Initiate directional inputs request
|
|
g.NavMoveDir = ImGuiDir.None
|
|
g.NavMoveFlags = ImGuiNavMoveFlags.None
|
|
g.NavMoveScrollFlags = ImGuiScrollFlags.None
|
|
if window and not g.NavWindowingTarget and bit32.band(window.Flags, ImGuiWindowFlags.NoNavInputs) == 0 then
|
|
local repeat_mode = bit32.bor(ImGuiInputFlags.Repeat, ImGuiInputFlags.RepeatRateNavMove)
|
|
if not ImGui.IsActiveIdUsingNavDir(ImGuiDir.Left) and ((nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.GamepadDpadLeft, repeat_mode, ImGuiKeyOwner_NoOwner)) or (nav_keyboard_active and ImGui.IsKeyPressedEx(ImGuiKey.LeftArrow, repeat_mode, ImGuiKeyOwner_NoOwner))) then g.NavMoveDir = ImGuiDir.Left end
|
|
if not ImGui.IsActiveIdUsingNavDir(ImGuiDir.Right) and ((nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.GamepadDpadRight, repeat_mode, ImGuiKeyOwner_NoOwner)) or (nav_keyboard_active and ImGui.IsKeyPressedEx(ImGuiKey.RightArrow, repeat_mode, ImGuiKeyOwner_NoOwner))) then g.NavMoveDir = ImGuiDir.Right end
|
|
if not ImGui.IsActiveIdUsingNavDir(ImGuiDir.Up) and ((nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.GamepadDpadUp, repeat_mode, ImGuiKeyOwner_NoOwner)) or (nav_keyboard_active and ImGui.IsKeyPressedEx(ImGuiKey.UpArrow, repeat_mode, ImGuiKeyOwner_NoOwner))) then g.NavMoveDir = ImGuiDir.Up end
|
|
if not ImGui.IsActiveIdUsingNavDir(ImGuiDir.Down) and ((nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.GamepadDpadDown, repeat_mode, ImGuiKeyOwner_NoOwner)) or (nav_keyboard_active and ImGui.IsKeyPressedEx(ImGuiKey.DownArrow, repeat_mode, ImGuiKeyOwner_NoOwner))) then g.NavMoveDir = ImGuiDir.Down end
|
|
end
|
|
g.NavMoveClipDir = g.NavMoveDir
|
|
ImRect_Copy(g.NavScoringNoClipRect, ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX))
|
|
end
|
|
|
|
-- Update PageUp/PageDown/Home/End scroll
|
|
-- FIXME-NAV: Consider enabling those keys even without the master ImGuiConfigFlags.NavEnableKeyboard flag?
|
|
local scoring_page_offset_y = 0.0
|
|
if window and g.NavMoveDir == ImGuiDir.None and nav_keyboard_active then
|
|
scoring_page_offset_y = ImGui.NavUpdatePageUpPageDown()
|
|
end
|
|
|
|
-- Submit
|
|
g.NavMoveForwardToNextFrame = false
|
|
if g.NavMoveDir ~= ImGuiDir.None then
|
|
ImGui.NavMoveRequestSubmit(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags)
|
|
end
|
|
|
|
-- Moving with no reference triggers an init request (will be used as a fallback if the direction fails to find a match)
|
|
if g.NavMoveSubmitted and g.NavId == 0 then
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from move, window \"%s\", layer=%d\n", window ? window->Name : "<NULL>", g.NavLayer)
|
|
g.NavInitRequest = true; g.NavInitRequestFromMove = true
|
|
g.NavInitResult.ID = 0
|
|
if g.IO.ConfigNavCursorVisibleAuto then -- NO check for _NoNavInputs here as we assume MoveRequests cannot be created.
|
|
g.NavCursorVisible = true
|
|
end
|
|
end
|
|
|
|
-- When using gamepad, we project the reference nav bounding box into window visible area.
|
|
-- This is to allow resuming navigation inside the visible area after doing a large amount of scrolling,
|
|
-- since with gamepad all movements are relative (can't focus a visible object like we can with the mouse).
|
|
if g.NavMoveSubmitted and g.NavInputSource == ImGuiInputSource.Gamepad and g.NavLayer == ImGuiNavLayer.Main and window ~= nil then -- and (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded))
|
|
local clamp_x = bit32.band(g.NavMoveFlags, bit32.bor(ImGuiNavMoveFlags.LoopX, ImGuiNavMoveFlags.WrapX)) == 0
|
|
local clamp_y = bit32.band(g.NavMoveFlags, bit32.bor(ImGuiNavMoveFlags.LoopY, ImGuiNavMoveFlags.WrapY)) == 0
|
|
local inner_rect_rel = ImGui.WindowRectAbsToRel(window, ImRect(ImVec2(window.InnerRect.Min.x - 1, window.InnerRect.Min.y - 1), ImVec2(window.InnerRect.Max.x + 1, window.InnerRect.Max.y + 1)))
|
|
|
|
-- Take account of changing scroll to handle triggering a new move request on a scrolling frame. (#6171)
|
|
-- Otherwise 'inner_rect_rel' would be off on the move result frame.
|
|
local next_scroll = CalcNextScrollFromScrollTargetAndClamp(window)
|
|
inner_rect_rel:Translate(ImVec2(next_scroll.x - window.Scroll.x, next_scroll.y - window.Scroll.y))
|
|
|
|
if (clamp_x or clamp_y) and not inner_rect_rel:Contains(window.NavRectRel[g.NavLayer]) then
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: clamp NavRectRel for gamepad move\n")
|
|
local pad_x = ImMin(inner_rect_rel:GetWidth(), window.FontRefSize * 0.5)
|
|
local pad_y = ImMin(inner_rect_rel:GetHeight(), window.FontRefSize * 0.5) -- Terrible approximation for the intent of starting navigation from first fully visible item
|
|
inner_rect_rel.Min.x = clamp_x and (inner_rect_rel.Min.x + pad_x) or -FLT_MAX
|
|
inner_rect_rel.Max.x = clamp_x and (inner_rect_rel.Max.x - pad_x) or FLT_MAX
|
|
inner_rect_rel.Min.y = clamp_y and (inner_rect_rel.Min.y + pad_y) or -FLT_MAX
|
|
inner_rect_rel.Max.y = clamp_y and (inner_rect_rel.Max.y - pad_y) or FLT_MAX
|
|
window.NavRectRel[g.NavLayer]:ClipWithFull(inner_rect_rel)
|
|
g.NavId = 0
|
|
end
|
|
end
|
|
|
|
-- Prepare scoring rectangle.
|
|
-- For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items)
|
|
local scoring_rect = ImRect()
|
|
if window ~= nil then
|
|
local nav_rect_rel = ImRect()
|
|
if not window.NavRectRel[g.NavLayer]:IsInverted() then
|
|
ImRect_Copy(nav_rect_rel, window.NavRectRel[g.NavLayer])
|
|
end
|
|
scoring_rect = ImGui.WindowRectRelToAbs(window, nav_rect_rel)
|
|
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsPageMove) ~= 0 then
|
|
-- When we start from a visible location, score visible items and prioritize this result.
|
|
if window.InnerRect:Contains(scoring_rect) then
|
|
g.NavMoveFlags = bit32.bor(g.NavMoveFlags, ImGuiNavMoveFlags.AlsoScoreVisibleSet)
|
|
end
|
|
ImRect_Copy(g.NavScoringNoClipRect, scoring_rect)
|
|
scoring_rect:TranslateY(scoring_page_offset_y)
|
|
g.NavScoringNoClipRect:Add(scoring_rect)
|
|
end
|
|
|
|
-- ImGui.GetForegroundDrawList():AddRectFilled(scoring_rect.Min - ImVec2(1, 1), scoring_rect.Max + ImVec2(1, 1), IM_COL32(255, 100, 0, 80)) -- [DEBUG] Pre-bias
|
|
if g.NavMoveSubmitted then
|
|
ImGui.NavBiasScoringRect(scoring_rect, window.RootWindowForNav.NavPreferredScoringPosRel[g.NavLayer], g.NavMoveDir, g.NavMoveFlags)
|
|
end
|
|
IM_ASSERT(not scoring_rect:IsInverted()) -- Ensure we have a non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem().
|
|
-- ImGui.GetForegroundDrawList():AddRectFilled(scoring_rect.Min - ImVec2(1, 1), scoring_rect.Max + ImVec2(1, 1), IM_COL32(255, 100, 0, 80)) -- [DEBUG] Post-bias
|
|
-- if not g.NavScoringNoClipRect:IsInverted() then ImGui.GetForegroundDrawList():AddRectFilled(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(100, 255, 0, 80)) end -- [DEBUG]
|
|
end
|
|
ImRect_Copy(g.NavScoringRect, scoring_rect)
|
|
-- g.NavScoringNoClipRect:Add(scoring_rect)
|
|
end
|
|
|
|
function ImGui.NavUpdateCreateTabbingRequest()
|
|
local g = GImGui
|
|
local window = g.NavWindow
|
|
IM_ASSERT(g.NavMoveDir == ImGuiDir.None)
|
|
if window == nil or g.NavWindowingTarget ~= nil or bit32.band(window.Flags, ImGuiWindowFlags.NoNavInputs) ~= 0 or not g.ConfigNavEnableTabbing then
|
|
return
|
|
end
|
|
|
|
local tab_pressed = ImGui.IsKeyPressedEx(ImGuiKey.Tab, ImGuiInputFlags.Repeat, ImGuiKeyOwner_NoOwner) and not g.IO.KeyCtrl and not g.IO.KeyAlt
|
|
if not tab_pressed then
|
|
return
|
|
end
|
|
|
|
-- Initiate tabbing request
|
|
-- (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags.NavEnableKeyboard flag!)
|
|
-- See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping.
|
|
local nav_keyboard_active = bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0
|
|
if nav_keyboard_active then
|
|
g.NavTabbingDir = g.IO.KeyShift and -1 or ((g.NavCursorVisible == false and g.ActiveId == 0) and 0 or 1)
|
|
else
|
|
g.NavTabbingDir = g.IO.KeyShift and -1 or ((g.ActiveId == 0) and 0 or 1)
|
|
end
|
|
local move_flags = bit32.bor(ImGuiNavMoveFlags.IsTabbing, ImGuiNavMoveFlags.Activate)
|
|
local scroll_flags = window.Appearing and bit32.bor(ImGuiScrollFlags.KeepVisibleEdgeX, ImGuiScrollFlags.AlwaysCenterY) or bit32.bor(ImGuiScrollFlags.KeepVisibleEdgeX, ImGuiScrollFlags.KeepVisibleEdgeY)
|
|
local clip_dir = (g.NavTabbingDir < 0) and ImGuiDir.Up or ImGuiDir.Down
|
|
ImGui.NavMoveRequestSubmit(ImGuiDir.None, clip_dir, move_flags, scroll_flags) -- FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable.
|
|
g.NavTabbingCounter = -1
|
|
end
|
|
|
|
function ImGui.NavUpdate()
|
|
local g = GImGui
|
|
local io = g.IO
|
|
|
|
io.WantSetMousePos = false
|
|
-- if g.NavScoringDebugCount > 0 then IMGUI_DEBUG_LOG_NAV("[nav] NavScoringDebugCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.NavScoringDebugCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest) end
|
|
|
|
-- Set input source based on which keys are last pressed (as some features differs when used with Gamepad vs Keyboard)
|
|
-- FIXME-NAV: Now that keys are separated maybe we can get rid of NavInputSource?
|
|
local nav_gamepad_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) ~= 0 and bit32.band(io.BackendFlags, ImGuiBackendFlags.HasGamepad) ~= 0
|
|
if nav_gamepad_active and g.NavInputSource ~= ImGuiInputSource.Gamepad then
|
|
for _, key in ipairs(nav_gamepad_keys_to_change_source) do
|
|
if ImGui.IsKeyDown(key) then
|
|
g.NavInputSource = ImGuiInputSource.Gamepad
|
|
end
|
|
end
|
|
end
|
|
|
|
local nav_keyboard_active = bit32.band(io.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0
|
|
if nav_keyboard_active and g.NavInputSource ~= ImGuiInputSource.Keyboard then
|
|
for _, key in ipairs(nav_keyboard_keys_to_change_source) do
|
|
if ImGui.IsKeyDown(key) then
|
|
g.NavInputSource = ImGuiInputSource.Keyboard
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Process navigation init request (select first/default focus)
|
|
g.NavJustMovedToId = 0
|
|
g.NavJustMovedToFocusScopeId = 0
|
|
g.NavJustMovedFromFocusScopeId = 0
|
|
if g.NavInitResult.ID ~= 0 then
|
|
ImGui.NavInitRequestApplyResult()
|
|
end
|
|
g.NavInitRequest = false
|
|
g.NavInitRequestFromMove = false
|
|
g.NavInitResult.ID = 0
|
|
|
|
-- Process navigation move request
|
|
if g.NavMoveSubmitted then
|
|
ImGui.NavMoveRequestApplyResult()
|
|
end
|
|
g.NavTabbingCounter = 0
|
|
g.NavMoveSubmitted = false
|
|
g.NavMoveScoringItems = false
|
|
if g.NavCursorHideFrames > 0 then
|
|
g.NavCursorHideFrames = g.NavCursorHideFrames - 1
|
|
if g.NavCursorHideFrames == 0 then
|
|
g.NavCursorVisible = true
|
|
end
|
|
end
|
|
|
|
-- Schedule mouse position update (will be done at the bottom of this function, after 1) processing all move requests and 2) updating scrolling)
|
|
local set_mouse_pos = false
|
|
if g.NavMousePosDirty and g.NavIdIsAlive then
|
|
if g.NavCursorVisible and g.NavHighlightItemUnderNav and g.NavWindow then
|
|
set_mouse_pos = true
|
|
end
|
|
end
|
|
g.NavMousePosDirty = false
|
|
IM_ASSERT(g.NavLayer == ImGuiNavLayer.Main or g.NavLayer == ImGuiNavLayer.Menu)
|
|
|
|
-- Store our return window (for returning from Menu Layer to Main Layer) and clear it as soon as we step back in our own Layer 0
|
|
if g.NavWindow then
|
|
ImGui.NavSaveLastChildNavWindowIntoParent(g.NavWindow)
|
|
end
|
|
if g.NavWindow and g.NavWindow.NavLastChildNavWindow ~= nil and g.NavLayer == ImGuiNavLayer.Main then
|
|
g.NavWindow.NavLastChildNavWindow = nil
|
|
end
|
|
|
|
-- Update Ctrl+Tab and Windowing features (hold Square to move/resize/etc.)
|
|
ImGui.NavUpdateWindowing()
|
|
|
|
-- Set output flags for user application
|
|
io.NavActive = (nav_keyboard_active or nav_gamepad_active) and g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) == 0
|
|
io.NavVisible = (io.NavActive and g.NavId ~= 0 and g.NavCursorVisible) or (g.NavWindowingTarget ~= nil)
|
|
|
|
-- Process NavCancel input (to close a popup, get back to parent, clear focus)
|
|
ImGui.NavUpdateCancelRequest()
|
|
ImGui.NavUpdateContextMenuRequest()
|
|
|
|
-- Process manual activation request
|
|
g.NavActivateId = 0
|
|
g.NavActivateDownId = 0
|
|
g.NavActivatePressedId = 0
|
|
g.NavActivateFlags = ImGuiActivateFlags.None
|
|
|
|
if g.NavId ~= 0 and g.NavCursorVisible and not g.NavWindowingTarget and g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) == 0 then
|
|
local activate_down = (nav_keyboard_active and ImGui.IsKeyDown(ImGuiKey.Space, ImGuiKeyOwner_NoOwner)) or (nav_gamepad_active and ImGui.IsKeyDown(ImGuiKey.NavGamepadActivate, ImGuiKeyOwner_NoOwner))
|
|
local activate_pressed = activate_down and ((nav_keyboard_active and ImGui.IsKeyPressedEx(ImGuiKey.Space, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner)) or (nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.NavGamepadActivate, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner)))
|
|
local input_pressed_keyboard = nav_keyboard_active and (ImGui.IsKeyPressedEx(ImGuiKey.Enter, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner) or ImGui.IsKeyPressedEx(ImGuiKey.KeypadEnter, ImGuiInputFlags.None, ImGuiKeyOwner_NoOwner))
|
|
local input_pressed_gamepad = false
|
|
if activate_down and nav_gamepad_active and ImGui.IsKeyDown(ImGuiKey.NavGamepadActivate, ImGuiKeyOwner_NoOwner) and bit32.band(g.NavIdItemFlags, ImGuiItemFlags.Inputable) ~= 0 then -- requires ImGuiItemFlags.Inputable to avoid retriggering regular buttons.
|
|
if ImGui.GetKeyData(g, ImGuiKey.NavGamepadActivate).DownDurationPrev < NAV_ACTIVATE_INPUT_WITH_GAMEPAD_DELAY and ImGui.GetKeyData(g, ImGuiKey.NavGamepadActivate).DownDuration >= NAV_ACTIVATE_INPUT_WITH_GAMEPAD_DELAY then
|
|
input_pressed_gamepad = true
|
|
end
|
|
end
|
|
|
|
if g.ActiveId == 0 and activate_pressed then
|
|
g.NavActivateId = g.NavId
|
|
g.NavActivateFlags = ImGuiActivateFlags.PreferTweak
|
|
end
|
|
if (g.ActiveId == 0 or g.ActiveId == g.NavId) and (input_pressed_keyboard or input_pressed_gamepad) then
|
|
g.NavActivateId = g.NavId
|
|
g.NavActivateFlags = ImGuiActivateFlags.PreferInput
|
|
end
|
|
if (g.ActiveId == 0 or g.ActiveId == g.NavId) and (activate_down or input_pressed_keyboard or input_pressed_gamepad) then -- FIXME-NAV: Unsure why input_pressed_xxx (migrated from input_down which was already dubious)
|
|
g.NavActivateDownId = g.NavId
|
|
end
|
|
if (g.ActiveId == 0 or g.ActiveId == g.NavId) and (activate_pressed or input_pressed_keyboard or input_pressed_gamepad) then
|
|
g.NavActivatePressedId = g.NavId
|
|
ImGui.NavHighlightActivated(g.NavId)
|
|
end
|
|
end
|
|
if g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) ~= 0 then
|
|
g.NavCursorVisible = false
|
|
elseif g.IO.ConfigNavCursorVisibleAlways and g.NavCursorHideFrames == 0 then
|
|
g.NavCursorVisible = true
|
|
end
|
|
if g.NavActivateId ~= 0 then
|
|
IM_ASSERT(g.NavActivateDownId == g.NavActivateId)
|
|
end
|
|
|
|
-- Highlight
|
|
if g.NavHighlightActivatedTimer > 0.0 then
|
|
g.NavHighlightActivatedTimer = ImMax(0.0, g.NavHighlightActivatedTimer - io.DeltaTime)
|
|
end
|
|
if g.NavHighlightActivatedTimer == 0.0 then
|
|
g.NavHighlightActivatedId = 0
|
|
end
|
|
|
|
-- Process programmatic activation request
|
|
-- FIXME-NAV: Those should eventually be queued (unlike focus they don't cancel each others)
|
|
if g.NavNextActivateId ~= 0 then
|
|
g.NavActivateId = g.NavNextActivateId
|
|
g.NavActivateDownId = g.NavNextActivateId
|
|
g.NavActivatePressedId = g.NavNextActivateId
|
|
g.NavActivateFlags = g.NavNextActivateFlags
|
|
end
|
|
g.NavNextActivateId = 0
|
|
|
|
-- Process move requests
|
|
ImGui.NavUpdateCreateMoveRequest()
|
|
if g.NavMoveDir == ImGuiDir.None then
|
|
ImGui.NavUpdateCreateTabbingRequest()
|
|
end
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
g.NavIdIsAlive = false
|
|
|
|
-- Scrolling
|
|
if g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) == 0 and not g.NavWindowingTarget then
|
|
-- *Fallback* manual-scroll with Nav directional keys when window has no navigable item
|
|
local window = g.NavWindow
|
|
local scroll_speed = IM_ROUND(window.FontRefSize * 100 * io.DeltaTime) -- We need round the scrolling speed because sub-pixel scroll isn't reliably supported.
|
|
local move_dir = g.NavMoveDir
|
|
if window.DC.NavLayersActiveMask == 0x00 and window.DC.NavWindowHasScrollY and move_dir ~= ImGuiDir.None then
|
|
if move_dir == ImGuiDir.Left or move_dir == ImGuiDir.Right then
|
|
local delta = (move_dir == ImGuiDir.Left) and -1.0 or 1.0
|
|
ImGui.SetScrollX(window, ImTrunc(window.Scroll.x + delta * scroll_speed))
|
|
end
|
|
if move_dir == ImGuiDir.Up or move_dir == ImGuiDir.Down then
|
|
local delta = (move_dir == ImGuiDir.Up) and -1.0 or 1.0
|
|
ImGui.SetScrollY(window, ImTrunc(window.Scroll.y + delta * scroll_speed))
|
|
end
|
|
end
|
|
|
|
-- *Normal* Manual scroll with LStick
|
|
-- Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds.
|
|
if nav_gamepad_active then
|
|
local scroll_dir = ImGui.GetKeyMagnitude2d(ImGuiKey.GamepadLStickLeft, ImGuiKey.GamepadLStickRight, ImGuiKey.GamepadLStickUp, ImGuiKey.GamepadLStickDown)
|
|
local tweak_factor = 1.0
|
|
if ImGui.IsKeyDown(ImGuiKey.NavGamepadTweakSlow) then
|
|
tweak_factor = 1.0 / 10.0
|
|
elseif ImGui.IsKeyDown(ImGuiKey.NavGamepadTweakFast) then
|
|
tweak_factor = 10.0
|
|
end
|
|
if scroll_dir.x ~= 0.0 and window.ScrollbarX then
|
|
ImGui.SetScrollX(window, ImTrunc(window.Scroll.x + scroll_dir.x * scroll_speed * tweak_factor))
|
|
end
|
|
if scroll_dir.y ~= 0.0 then
|
|
ImGui.SetScrollY(window, ImTrunc(window.Scroll.y + scroll_dir.y * scroll_speed * tweak_factor))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Always prioritize mouse highlight if navigation is disabled
|
|
if not nav_keyboard_active and not nav_gamepad_active then
|
|
g.NavCursorVisible = false
|
|
g.NavHighlightItemUnderNav = false
|
|
set_mouse_pos = false
|
|
end
|
|
|
|
-- Update mouse position if requested
|
|
-- (This will take into account the possibility that a Scroll was queued in the window to offset our absolute mouse position before scroll has been applied)
|
|
if set_mouse_pos and io.ConfigNavMoveSetMousePos and bit32.band(io.BackendFlags, ImGuiBackendFlags.HasSetMousePos) ~= 0 then
|
|
ImGui.TeleportMousePos(ImGui.NavCalcPreferredRefPos(ImGuiWindowFlags.Popup))
|
|
end
|
|
|
|
-- [DEBUG]
|
|
g.NavScoringDebugCount = 0
|
|
end
|
|
|
|
end
|
|
|
|
-- Process Escape/NavCancel input (to close a popup, get back to parent, clear focus)
|
|
-- FIXME: In order to support e.g. Escape to clear a selection we'll need:
|
|
-- - either to store the equivalent of ActiveIdUsingKeyInputMask for a FocusScope and test for it.
|
|
-- - either to move most/all of those tests to the epilogue/end functions of the scope they are dealing with (e.g. exit child window in EndChild()) or in EndFrame(), to allow an earlier intercept
|
|
function ImGui.NavUpdateCancelRequest()
|
|
local g = GImGui
|
|
local nav_gamepad_active = bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableGamepad) ~= 0 and bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.HasGamepad) ~= 0
|
|
local nav_keyboard_active = bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0
|
|
if not (nav_keyboard_active and ImGui.IsKeyPressedEx(ImGuiKey.Escape, 0, ImGuiKeyOwner_NoOwner)) and not (nav_gamepad_active and ImGui.IsKeyPressedEx(ImGuiKey.NavGamepadCancel, 0, ImGuiKeyOwner_NoOwner)) then
|
|
return
|
|
end
|
|
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavUpdateCancelRequest()")
|
|
if g.ActiveId ~= 0 then
|
|
ImGui.ClearActiveID()
|
|
elseif g.NavLayer ~= ImGuiNavLayer.Main then
|
|
-- Leave the "menu" layer
|
|
ImGui.NavRestoreLayer(ImGuiNavLayer.Main)
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
elseif g.NavWindow and g.NavWindow ~= g.NavWindow.RootWindow and bit32.band(g.NavWindow.RootWindowForNav.Flags, ImGuiWindowFlags.Popup) == 0 and g.NavWindow.RootWindowForNav.ParentWindow then
|
|
-- Exit child window
|
|
local child_window = g.NavWindow.RootWindowForNav
|
|
local parent_window = child_window.ParentWindow
|
|
IM_ASSERT(child_window.ChildId ~= 0)
|
|
ImGui.FocusWindow(parent_window)
|
|
ImGui.SetNavID(child_window.ChildId, ImGuiNavLayer.Main, 0, ImGui.WindowRectAbsToRel(parent_window, child_window:Rect()))
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
elseif g.OpenPopupStack.Size > 0 and g.OpenPopupStack:back().Window ~= nil and bit32.band(g.OpenPopupStack:back().Window.Flags, ImGuiWindowFlags.Modal) == 0 then
|
|
-- Close open popup/menu
|
|
ImGui.ClosePopupToLevel(g.OpenPopupStack.Size - 1, true)
|
|
else
|
|
-- Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were
|
|
-- FIXME-NAV: This should happen on window appearing.
|
|
if g.IO.ConfigNavEscapeClearFocusItem or g.IO.ConfigNavEscapeClearFocusWindow then
|
|
if g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.Popup) ~= 0 then -- or not (g.NavWindow.Flags & ImGuiWindowFlags.ChildWindow)
|
|
g.NavWindow.NavLastIds[0] = 0
|
|
end
|
|
end
|
|
|
|
-- Clear nav focus
|
|
if g.IO.ConfigNavEscapeClearFocusItem or g.IO.ConfigNavEscapeClearFocusWindow then
|
|
g.NavId = 0
|
|
end
|
|
if g.IO.ConfigNavEscapeClearFocusWindow then
|
|
ImGui.FocusWindow(nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.NavMoveRequestButNoResultYet()
|
|
local g = GImGui
|
|
return g.NavMoveScoringItems and g.NavMoveResultLocal.ID == 0 and g.NavMoveResultOther.ID == 0
|
|
end
|
|
|
|
local function ImGetDirQuadrantFromDelta(dx, dy)
|
|
if ImFabs(dx) > ImFabs(dy) then
|
|
return (dx > 0.0) and ImGuiDir.Right or ImGuiDir.Left
|
|
end
|
|
return (dy > 0.0) and ImGuiDir.Down or ImGuiDir.Up
|
|
end
|
|
|
|
local function NavScoreItemDistInterval(cand_min, cand_max, curr_min, curr_max)
|
|
if cand_max < curr_min then
|
|
return cand_max - curr_min
|
|
end
|
|
if curr_max < cand_min then
|
|
return cand_min - curr_max
|
|
end
|
|
return 0.0
|
|
end
|
|
|
|
-- Scoring function for keyboard/gamepad directional navigation. Based on https://gist.github.com/rygorous/6981057
|
|
--- @param result ImGuiNavItemData
|
|
--- @param nav_bb ImRect
|
|
--- @return bool
|
|
function ImGui.NavScoreItem(result, nav_bb)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if g.NavLayer ~= window.DC.NavLayerCurrent then
|
|
return false
|
|
end
|
|
|
|
-- FIXME: Those are not good variables names
|
|
local cand = ImRect()
|
|
ImRect_Copy(cand, nav_bb) -- Current item nav rectangle
|
|
local curr = g.NavScoringRect -- Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width)
|
|
g.NavScoringDebugCount = g.NavScoringDebugCount + 1
|
|
|
|
-- When entering through a NavFlattened border, we consider child window items as fully clipped for scoring
|
|
if window.ParentWindow == g.NavWindow then
|
|
IM_ASSERT(bit32.band(bit32.bor(window.ChildFlags, g.NavWindow.ChildFlags), ImGuiChildFlags.NavFlattened) ~= 0)
|
|
if not window.ClipRect:Overlaps(cand) then
|
|
return false
|
|
end
|
|
cand:ClipWithFull(window.ClipRect) -- This allows the scored item to not overlap other candidates in the parent window
|
|
end
|
|
|
|
-- Compute distance between boxes
|
|
-- FIXME-NAV: Introducing biases for vertical navigation, needs to be removed.
|
|
local dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x)
|
|
local dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2), ImLerp(cand.Min.y, cand.Max.y, 0.8), ImLerp(curr.Min.y, curr.Max.y, 0.2), ImLerp(curr.Min.y, curr.Max.y, 0.8)) -- Scale down on Y to keep using box-distance for vertically touching items
|
|
if dby ~= 0.0 and dbx ~= 0.0 then
|
|
dbx = (dbx / 1000.0) + ((dbx > 0.0) and 1.0 or -1.0)
|
|
end
|
|
local dist_box = ImFabs(dbx) + ImFabs(dby)
|
|
|
|
-- Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter)
|
|
local dcx = (cand.Min.x + cand.Max.x) - (curr.Min.x + curr.Max.x)
|
|
local dcy = (cand.Min.y + cand.Max.y) - (curr.Min.y + curr.Max.y)
|
|
local dist_center = ImFabs(dcx) + ImFabs(dcy) -- L1 metric (need this for our connectedness guarantee)
|
|
|
|
-- Determine which quadrant of 'curr' our candidate item 'cand' lies in based on distance
|
|
local quadrant
|
|
local dax = 0.0
|
|
local day = 0.0
|
|
local dist_axial = 0.0
|
|
if dbx ~= 0.0 or dby ~= 0.0 then
|
|
-- For non-overlapping boxes, use distance between boxes
|
|
-- FIXME-NAV: Quadrant may be incorrect because of (1) dbx bias and (2) curr.Max.y bias applied by NavBiasScoringRect() where typically curr.Max.y==curr.Min.y
|
|
-- One typical case where this happens, with style.WindowMenuButtonPosition == ImGuiDir.Right, pressing Left to navigate from Close to Collapse tends to fail.
|
|
-- Also see #6344. Calling ImGetDirQuadrantFromDelta() with unbiased values may be good but side-effects are plenty.
|
|
dax = dbx
|
|
day = dby
|
|
dist_axial = dist_box
|
|
quadrant = ImGetDirQuadrantFromDelta(dbx, dby)
|
|
elseif dcx ~= 0.0 or dcy ~= 0.0 then
|
|
-- For overlapping boxes with different centers, use distance between centers
|
|
dax = dcx
|
|
day = dcy
|
|
dist_axial = dist_center
|
|
quadrant = ImGetDirQuadrantFromDelta(dcx, dcy)
|
|
else
|
|
-- Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter)
|
|
quadrant = (g.LastItemData.ID < g.NavId) and ImGuiDir.Left or ImGuiDir.Right
|
|
end
|
|
|
|
local move_dir = g.NavMoveDir
|
|
|
|
-- Is it in the quadrant we're interested in moving to?
|
|
local new_best = false
|
|
if quadrant == move_dir then
|
|
-- Does it beat the current best candidate?
|
|
if dist_box < result.DistBox then
|
|
result.DistBox = dist_box
|
|
result.DistCenter = dist_center
|
|
return true
|
|
end
|
|
if dist_box == result.DistBox then
|
|
-- Try using distance between center points to break ties
|
|
if dist_center < result.DistCenter then
|
|
result.DistCenter = dist_center
|
|
new_best = true
|
|
elseif dist_center == result.DistCenter then
|
|
-- Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items
|
|
-- (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index),
|
|
-- this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis.
|
|
if (((move_dir == ImGuiDir.Up or move_dir == ImGuiDir.Down) and dby) or dbx) < 0.0 then -- moving bj to the right/down decreases distance
|
|
new_best = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Axial check: if 'curr' has no link at all in some direction and 'cand' lies roughly in that direction, add a tentative link. This will only be kept if no "real" matches
|
|
-- are found, so it only augments the graph produced by the above method using extra links. (important, since it doesn't guarantee strong connectedness)
|
|
-- This is just to avoid buttons having no links in a particular direction when there's a suitable neighbor. you get good graphs without this too.
|
|
-- 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward.
|
|
-- Disabling it may lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option?
|
|
if result.DistBox == FLT_MAX and dist_axial < result.DistAxial then -- Check axial match
|
|
if g.NavLayer == ImGuiNavLayer.Menu and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.ChildMenu) == 0 then
|
|
if (move_dir == ImGuiDir.Left and dax < 0.0) or (move_dir == ImGuiDir.Right and dax > 0.0) or (move_dir == ImGuiDir.Up and day < 0.0) or (move_dir == ImGuiDir.Down and day > 0.0) then
|
|
result.DistAxial = dist_axial
|
|
new_best = true
|
|
end
|
|
end
|
|
end
|
|
|
|
return new_best
|
|
end
|
|
|
|
--- @param result ImGuiNavItemData
|
|
function ImGui.NavApplyItemToResult(result)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
result.Window = window
|
|
result.ID = g.LastItemData.ID
|
|
result.FocusScopeId = g.CurrentFocusScopeId
|
|
result.ItemFlags = g.LastItemData.ItemFlags
|
|
ImRect_Copy(result.RectRel, ImGui.WindowRectAbsToRel(window, g.LastItemData.NavRect))
|
|
if bit32.band(result.ItemFlags, ImGuiItemFlags.HasSelectionUserData) ~= 0 then
|
|
IM_ASSERT(g.NextItemData.SelectionUserData ~= ImGuiSelectionUserData_Invalid)
|
|
result.SelectionUserData = g.NextItemData.SelectionUserData -- INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData.
|
|
end
|
|
end
|
|
|
|
--- @param result ImGuiNavItemData
|
|
function ImGui.NavMoveRequestResolveWithLastItem(result)
|
|
local g = GImGui
|
|
g.NavMoveScoringItems = false -- Ensure request doesn't need more processing
|
|
ImGui.NavApplyItemToResult(result)
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
|
|
-- Handle "scoring" of an item for a tabbing/focusing request initiated by NavUpdateCreateTabbingRequest().
|
|
-- Note that SetKeyboardFocusHere() API calls are considered tabbing requests!
|
|
-- - Case 1: no nav/active id: set result to first eligible item, stop storing.
|
|
-- - Case 2: tab forward: on ref id set counter, on counter elapse store result
|
|
-- - Case 3: tab forward wrap: set result to first eligible item (preemptively), on ref id set counter, on next frame if counter hasn't elapsed store result. -- FIXME-TABBING: Could be done as a next-frame forwarded request
|
|
-- - Case 4: tab backward: store all results, on ref id pick prev, stop storing
|
|
-- - Case 5: tab backward wrap: store all results, on ref id if no result keep storing until last -- FIXME-TABBING: Could be done as next-frame forwarded requested
|
|
--- @param id ImGuiID
|
|
--- @param item_flags ImGuiItemFlags
|
|
--- @param move_flags ImGuiNavMoveFlags
|
|
function ImGui.NavProcessItemForTabbingRequest(id, item_flags, move_flags)
|
|
local g = GImGui
|
|
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.FocusApi) == 0 then
|
|
if g.NavLayer ~= g.CurrentWindow.DC.NavLayerCurrent then
|
|
return
|
|
end
|
|
if g.NavFocusScopeId ~= g.CurrentFocusScopeId then
|
|
return
|
|
end
|
|
end
|
|
|
|
-- - Can always land on an item when using API call.
|
|
-- - Tabbing with _NavEnableKeyboard (space/enter/arrows): goes through every item.
|
|
-- - Tabbing without _NavEnableKeyboard: goes through inputable items only.
|
|
local can_stop
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.FocusApi) ~= 0 then
|
|
can_stop = true
|
|
else
|
|
can_stop = bit32.band(item_flags, ImGuiItemFlags.NoTabStop) == 0 and (bit32.band(g.IO.ConfigFlags, ImGuiConfigFlags.NavEnableKeyboard) ~= 0 or bit32.band(item_flags, ImGuiItemFlags.Inputable) ~= 0)
|
|
end
|
|
|
|
-- Always store in NavMoveResultLocal (unlike directional request which uses NavMoveResultOther on sibling/flattened windows)
|
|
local result = g.NavMoveResultLocal
|
|
if g.NavTabbingDir == 1 then
|
|
-- Tab Forward or SetKeyboardFocusHere() with >= 0
|
|
if can_stop and g.NavTabbingResultFirst.ID == 0 then
|
|
ImGui.NavApplyItemToResult(g.NavTabbingResultFirst)
|
|
end
|
|
if can_stop and g.NavTabbingCounter > 0 then
|
|
g.NavTabbingCounter = g.NavTabbingCounter - 1
|
|
if g.NavTabbingCounter == 0 then
|
|
ImGui.NavMoveRequestResolveWithLastItem(result)
|
|
end
|
|
elseif g.NavId == id then
|
|
g.NavTabbingCounter = 1
|
|
end
|
|
elseif g.NavTabbingDir == -1 then
|
|
-- Tab Backward
|
|
if g.NavId == id then
|
|
if result.ID ~= 0 then
|
|
g.NavMoveScoringItems = false
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
elseif can_stop then
|
|
-- Keep applying until reaching NavId
|
|
ImGui.NavApplyItemToResult(result)
|
|
end
|
|
elseif g.NavTabbingDir == 0 then
|
|
if can_stop and g.NavId == id then
|
|
ImGui.NavMoveRequestResolveWithLastItem(result)
|
|
end
|
|
if can_stop and g.NavTabbingResultFirst.ID == 0 then -- Tab init
|
|
ImGui.NavApplyItemToResult(g.NavTabbingResultFirst)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above)
|
|
-- This is called after LastItemData is set, but NextItemData is also still valid.
|
|
function ImGui.NavProcessItem()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local id = g.LastItemData.ID
|
|
local item_flags = g.LastItemData.ItemFlags
|
|
|
|
-- When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221, #8816, #7994)
|
|
local nav_bb = ImRect()
|
|
ImRect_Copy(nav_bb, g.LastItemData.NavRect)
|
|
if window.DC.NavIsScrollPushableX == false then
|
|
nav_bb.Min.x = ImClamp(nav_bb.Min.x, window.ClipRect.Min.x, window.ClipRect.Max.x)
|
|
nav_bb.Max.x = ImClamp(nav_bb.Max.x, window.ClipRect.Min.x, window.ClipRect.Max.x)
|
|
end
|
|
|
|
-- Process Init Request
|
|
if g.NavInitRequest and g.NavLayer == window.DC.NavLayerCurrent and bit32.band(item_flags, ImGuiItemFlags.Disabled) == 0 then
|
|
-- Even if 'ImGuiItemFlags.NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback
|
|
local candidate_for_nav_default_focus = bit32.band(item_flags, ImGuiItemFlags.NoNavDefaultFocus) == 0
|
|
if candidate_for_nav_default_focus or g.NavInitResult.ID == 0 then
|
|
ImGui.NavApplyItemToResult(g.NavInitResult)
|
|
end
|
|
if candidate_for_nav_default_focus then
|
|
g.NavInitRequest = false -- Found a match, clear request
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
end
|
|
|
|
-- Process Move Request (scoring for navigation)
|
|
-- FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy)
|
|
if g.NavMoveScoringItems and bit32.band(item_flags, ImGuiItemFlags.Disabled) == 0 then
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.FocusApi) ~= 0 or bit32.band(window.Flags, ImGuiWindowFlags.NoNavInputs) == 0 then
|
|
local is_tabbing = bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0
|
|
if is_tabbing then
|
|
ImGui.NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags)
|
|
elseif g.NavId ~= id or bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.AllowCurrentNavId) ~= 0 then
|
|
local result = (window == g.NavWindow) and g.NavMoveResultLocal or g.NavMoveResultOther
|
|
if ImGui.NavScoreItem(result, nav_bb) then
|
|
ImGui.NavApplyItemToResult(result)
|
|
end
|
|
|
|
-- Features like PageUp/PageDown need to maintain a separate score for the visible set of items.
|
|
local VISIBLE_RATIO = 0.70
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.AlsoScoreVisibleSet) ~= 0 then
|
|
local r = window.InnerRect -- window.ClipRect
|
|
if r:Overlaps(nav_bb) then
|
|
if ImClamp(nav_bb.Max.y, r.Min.y, r.Max.y) - ImClamp(nav_bb.Min.y, r.Min.y, r.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO then
|
|
if ImGui.NavScoreItem(g.NavMoveResultLocalVisible, nav_bb) then
|
|
ImGui.NavApplyItemToResult(g.NavMoveResultLocalVisible)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update information for currently focused/navigated item
|
|
if g.NavId == id then
|
|
if g.NavWindow ~= window then
|
|
ImGui.SetNavWindow(window) -- Always refresh g.NavWindow, because some operations such as FocusItem() may not have a window.
|
|
end
|
|
g.NavLayer = window.DC.NavLayerCurrent
|
|
ImGui.SetNavFocusScope(g.CurrentFocusScopeId) -- Will set g.NavFocusScopeId AND store g.NavFocusScopePath
|
|
g.NavFocusScopeId = g.CurrentFocusScopeId
|
|
g.NavIdIsAlive = true
|
|
g.NavIdItemFlags = item_flags
|
|
if bit32.band(g.LastItemData.ItemFlags, ImGuiItemFlags.HasSelectionUserData) ~= 0 then
|
|
IM_ASSERT(g.NextItemData.SelectionUserData ~= ImGuiSelectionUserData_Invalid)
|
|
g.NavLastValidSelectionUserData = g.NextItemData.SelectionUserData -- INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData.
|
|
end
|
|
window.NavRectRel[window.DC.NavLayerCurrent] = ImGui.WindowRectAbsToRel(window, nav_bb) -- Store item bounding box (relative to window position)
|
|
end
|
|
end
|
|
|
|
function ImGui.NavInitRequestApplyResult()
|
|
-- In very rare cases g.NavWindow may be null (e.g. clearing focus after requesting an init request, which does happen when releasing Alt while clicking on void)
|
|
local g = GImGui
|
|
if not g.NavWindow then
|
|
return
|
|
end
|
|
|
|
local result = g.NavInitResult
|
|
if g.NavId ~= result.ID then
|
|
g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId
|
|
g.NavJustMovedToId = result.ID
|
|
g.NavJustMovedToFocusScopeId = result.FocusScopeId
|
|
g.NavJustMovedToKeyMods = 0
|
|
g.NavJustMovedToIsTabbing = false
|
|
g.NavJustMovedToHasSelectionData = bit32.band(result.ItemFlags, ImGuiItemFlags.HasSelectionUserData) ~= 0
|
|
end
|
|
|
|
-- Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called)
|
|
-- FIXME-NAV: On _NavFlattened windows, g.NavWindow will only be updated during subsequent frame. Not a problem currently.
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: ApplyResult: NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name)
|
|
ImGui.SetNavID(result.ID, g.NavLayer, result.FocusScopeId, result.RectRel)
|
|
g.NavIdIsAlive = true -- Mark as alive from previous frame as we got a result
|
|
if result.SelectionUserData ~= ImGuiSelectionUserData_Invalid then
|
|
g.NavLastValidSelectionUserData = result.SelectionUserData
|
|
end
|
|
if g.NavInitRequestFromMove then
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
end
|
|
end
|
|
|
|
-- Apply result from previous frame navigation directional move request. Always called from NavUpdate()
|
|
function ImGui.NavMoveRequestApplyResult()
|
|
local g = GImGui
|
|
|
|
-- Select which result to use
|
|
local result
|
|
if g.NavMoveResultLocal.ID ~= 0 then
|
|
result = g.NavMoveResultLocal
|
|
elseif g.NavMoveResultOther.ID ~= 0 then
|
|
result = g.NavMoveResultOther
|
|
else
|
|
result = nil
|
|
end
|
|
|
|
-- Tabbing forward wrap
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0 and result == nil then
|
|
if (g.NavTabbingCounter == 1 or g.NavTabbingDir == 0) and g.NavTabbingResultFirst.ID ~= 0 then
|
|
result = g.NavTabbingResultFirst
|
|
end
|
|
end
|
|
|
|
-- In a situation when there are no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result)
|
|
local axis = (g.NavMoveDir == ImGuiDir.Up or g.NavMoveDir == ImGuiDir.Down) and ImGuiAxis.Y or ImGuiAxis.X
|
|
if result == nil then
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0 then
|
|
g.NavMoveFlags = bit32.bor(g.NavMoveFlags, ImGuiNavMoveFlags.NoSetNavCursorVisible)
|
|
end
|
|
if g.NavId ~= 0 and bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.NoSetNavCursorVisible) == 0 then
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
end
|
|
ImGui.NavClearPreferredPosForAxis(axis) -- On a failed move, clear preferred pos for this axis.
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavMoveSubmitted but not led to a result!\n")
|
|
return
|
|
end
|
|
|
|
-- PageUp/PageDown behavior first jumps to the bottom/top mostly visible item, _otherwise_ use the result from the previous/next page.
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.AlsoScoreVisibleSet) ~= 0
|
|
and g.NavMoveResultLocalVisible.ID ~= 0 and g.NavMoveResultLocalVisible.ID ~= g.NavId then
|
|
result = g.NavMoveResultLocalVisible
|
|
end
|
|
|
|
-- Maybe entering a flattened child from the outside? In this case solve the tie using the regular scoring rules.
|
|
if result ~= g.NavMoveResultOther and g.NavMoveResultOther.ID ~= 0 and g.NavMoveResultOther.Window.ParentWindow == g.NavWindow then
|
|
if g.NavMoveResultOther.DistBox < result.DistBox
|
|
or (g.NavMoveResultOther.DistBox == result.DistBox and g.NavMoveResultOther.DistCenter < result.DistCenter) then
|
|
result = g.NavMoveResultOther
|
|
end
|
|
end
|
|
IM_ASSERT(g.NavWindow and result.Window)
|
|
|
|
-- Scroll to keep newly navigated item fully into view.
|
|
if g.NavLayer == ImGuiNavLayer.Main then
|
|
local rect_abs = ImGui.WindowRectRelToAbs(result.Window, result.RectRel)
|
|
ImGui.ScrollToRectEx(result.Window, rect_abs, g.NavMoveScrollFlags)
|
|
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.ScrollToEdgeY) ~= 0 then
|
|
-- FIXME: Should remove this? Or make more precise: use ScrollToRectEx() with edge?
|
|
local scroll_target = (g.NavMoveDir == ImGuiDir.Up) and result.Window.ScrollMax.y or 0.0
|
|
ImGui.SetScrollY(result.Window, scroll_target)
|
|
end
|
|
end
|
|
|
|
if g.NavWindow ~= result.Window then
|
|
-- IMGUI_DEBUG_LOG_FOCUS("[focus] NavMoveRequest: SetNavWindow(\"%s\")\n", result->Window->Name)
|
|
g.NavWindow = result.Window
|
|
g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid
|
|
end
|
|
|
|
-- Clear active id unless requested not to
|
|
-- FIXME: ImGuiNavMoveFlags.NoClearActiveId is currently unused as we don't have a clear strategy to preserve active id after interaction,
|
|
-- so this is mostly provided as a gateway for further experiments (see #1418, #2890)
|
|
if g.ActiveId ~= result.ID and bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.NoClearActiveId) == 0 then
|
|
ImGui.ClearActiveID()
|
|
end
|
|
|
|
-- Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags.AllowCurrentNavId)
|
|
-- PageUp/PageDown however sets always set NavJustMovedTo (vs Home/End which doesn't) mimicking Windows behavior.
|
|
if (g.NavId ~= result.ID or bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsPageMove) ~= 0)
|
|
and bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.NoSelect) == 0 then
|
|
g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId
|
|
g.NavJustMovedToId = result.ID
|
|
g.NavJustMovedToFocusScopeId = result.FocusScopeId
|
|
g.NavJustMovedToKeyMods = g.NavMoveKeyMods
|
|
g.NavJustMovedToIsTabbing = bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0
|
|
g.NavJustMovedToHasSelectionData = bit32.band(result.ItemFlags, ImGuiItemFlags.HasSelectionUserData) ~= 0
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavJustMovedFromFocusScopeId = 0x%08X, NavJustMovedToFocusScopeId = 0x%08X\n", g.NavJustMovedFromFocusScopeId, g.NavJustMovedToFocusScopeId)
|
|
end
|
|
|
|
-- Apply new NavID/Focus
|
|
-- IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name)
|
|
local root_for_nav = g.NavWindow.RootWindowForNav
|
|
local preferred_scoring_pos_rel = ImVec2()
|
|
ImVec2_Copy(preferred_scoring_pos_rel, root_for_nav.NavPreferredScoringPosRel[g.NavLayer])
|
|
ImGui.SetNavID(result.ID, g.NavLayer, result.FocusScopeId, result.RectRel)
|
|
if result.SelectionUserData ~= ImGuiSelectionUserData_Invalid then
|
|
g.NavLastValidSelectionUserData = result.SelectionUserData
|
|
end
|
|
|
|
-- Restore last preferred position for current axis
|
|
-- (storing in RootWindowForNav-> as the info is desirable at the beginning of a Move Request. In theory all storage should use RootWindowForNav..)
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) == 0 then
|
|
preferred_scoring_pos_rel[axis] = result.RectRel:GetCenter()[axis]
|
|
ImVec2_Copy(root_for_nav.NavPreferredScoringPosRel[g.NavLayer], preferred_scoring_pos_rel)
|
|
end
|
|
|
|
-- Tabbing: Activates Inputable, otherwise only Focus
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0
|
|
and bit32.band(result.ItemFlags, ImGuiItemFlags.Inputable) == 0 then
|
|
g.NavMoveFlags = bit32.band(g.NavMoveFlags, bit32.bnot(ImGuiNavMoveFlags.Activate))
|
|
end
|
|
|
|
-- Activate
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.Activate) ~= 0 then
|
|
g.NavNextActivateId = result.ID
|
|
g.NavNextActivateFlags = ImGuiActivateFlags.None
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.FocusApi) ~= 0 then
|
|
g.NavNextActivateFlags = bit32.bor(g.NavNextActivateFlags, ImGuiActivateFlags.FromFocusApi)
|
|
end
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0 then
|
|
g.NavNextActivateFlags = bit32.bor(g.NavNextActivateFlags, ImGuiActivateFlags.PreferInput, ImGuiActivateFlags.TryToPreserveState, ImGuiActivateFlags.FromTabbing)
|
|
end
|
|
end
|
|
|
|
-- Make nav cursor visible
|
|
if bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.NoSetNavCursorVisible) == 0 then
|
|
ImGui.SetNavCursorVisibleAfterMove()
|
|
end
|
|
end
|
|
|
|
-- True when current work location may be scrolled horizontally when moving left / right.
|
|
-- This is generally always true UNLESS within a column. We don't have a vertical equivalent.
|
|
function ImGui.NavUpdateCurrentWindowIsScrollPushableX()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
window.DC.NavIsScrollPushableX = (g.CurrentTable == nil and window.DC.CurrentColumns == nil)
|
|
end
|
|
|
|
-- Called by TreePop() to implement ImGuiTreeNodeFlags.NavLeftJumpsToParent
|
|
--- @param result ImGuiNavItemData
|
|
--- @param tree_node_data ImGuiTreeNodeStackData
|
|
function ImGui.NavMoveRequestResolveWithPastTreeNode(result, tree_node_data)
|
|
local g = GImGui
|
|
g.NavMoveScoringItems = false
|
|
g.LastItemData.ID = tree_node_data.ID
|
|
g.LastItemData.ItemFlags = bit32.band(tree_node_data.ItemFlags, bit32.bnot(ImGuiItemFlags.HasSelectionUserData)) -- Losing SelectionUserData, recovered next-frame (cheaper).
|
|
ImRect_Copy(g.LastItemData.NavRect, tree_node_data.NavRect)
|
|
ImGui.NavApplyItemToResult(result) -- Result this instead of implementing a NavApplyPastTreeNodeToResult()
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.Y)
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
|
|
function ImGui.NavMoveRequestCancel()
|
|
local g = GImGui
|
|
g.NavMoveSubmitted = false
|
|
g.NavMoveScoringItems = false
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
|
|
-- Forward will reuse the move request again on the next frame (generally with modifications done to it)
|
|
--- @param move_dir ImGuiDir
|
|
--- @param clip_dir ImGuiDir
|
|
--- @param move_flags ImGuiNavMoveFlags
|
|
--- @param scroll_flags ImGuiScrollFlags
|
|
function ImGui.NavMoveRequestForward(move_dir, clip_dir, move_flags, scroll_flags)
|
|
local g = GImGui
|
|
IM_ASSERT(g.NavMoveForwardToNextFrame == false)
|
|
ImGui.NavMoveRequestCancel()
|
|
g.NavMoveForwardToNextFrame = true
|
|
g.NavMoveDir = move_dir
|
|
g.NavMoveClipDir = clip_dir
|
|
g.NavMoveFlags = bit32.bor(move_flags, ImGuiNavMoveFlags.Forwarded)
|
|
g.NavMoveScrollFlags = scroll_flags
|
|
end
|
|
|
|
-- Navigation wrap-around logic is delayed to the end of the frame because this operation is only valid after entire
|
|
-- popup is assembled and in case of appended popups it is not clear which EndPopup() call is final.
|
|
--- @param window ImGuiWindow
|
|
--- @param wrap_flags ImGuiNavMoveFlags
|
|
function ImGui.NavMoveRequestTryWrapping(window, wrap_flags)
|
|
local g = GImGui
|
|
IM_ASSERT(bit32.band(wrap_flags, ImGuiNavMoveFlags.WrapMask_) ~= 0 and bit32.band(wrap_flags, bit32.bnot(ImGuiNavMoveFlags.WrapMask_)) == 0) -- Call with _WrapX, _WrapY, _LoopX, _LoopY
|
|
|
|
-- In theory we should test for NavMoveRequestButNoResultYet() but there's no point doing it:
|
|
-- as NavEndFrame() will do the same test. It will end up calling NavUpdateCreateWrappingRequest().
|
|
if g.NavWindow == window and g.NavMoveScoringItems and g.NavLayer == window.DC.NavLayerCurrent then
|
|
g.NavMoveFlags = bit32.bor(bit32.band(g.NavMoveFlags, bit32.bnot(ImGuiNavMoveFlags.WrapMask_)), wrap_flags)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] DRAG AND DROP
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.IsDragDropPayloadBeingAccepted()
|
|
local g = GImGui
|
|
return g.DragDropActive and g.DragDropAcceptIdPrev ~= 0
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] LOCALIZATION
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param entries ImGuiLocEntry[]
|
|
--- @param count int
|
|
function ImGui.LocalizeRegisterEntries(entries, count)
|
|
local g = GImGui
|
|
for n = 1, count do
|
|
g.LocalizationTable[entries[n].Key] = entries[n].Text
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] VIEWPORTS, PLATFORM WINDOWS
|
|
-------------------------------------------------------------------------------
|
|
-- - GetMainViewport()
|
|
-- - FindViewportByID()
|
|
-- - SetCurrentViewport() [Internal]
|
|
-- - SetWindowViewport() [Internal]
|
|
-- - GetWindowAlwaysWantOwnViewport() [Internal]
|
|
-- - UpdateTryMergeWindowIntoHostViewport() [Internal]
|
|
-- - UpdateTryMergeWindowIntoHostViewports() [Internal]
|
|
-- - TranslateWindowsInViewport() [Internal]
|
|
-- - ScaleWindowsInViewport() [Internal]
|
|
-- - FindHoveredViewportFromPlatformWindowStack() [Internal]
|
|
-- - UpdateViewportsNewFrame() [Internal]
|
|
-- - UpdateViewportsEndFrame() [Internal]
|
|
-- - AddUpdateViewport() [Internal]
|
|
-- - WindowSelectViewport() [Internal]
|
|
-- - WindowSyncOwnedViewport() [Internal]
|
|
-- - UpdatePlatformWindows()
|
|
-- - RenderPlatformWindowsDefault()
|
|
-- - FindPlatformMonitorForPos() [Internal]
|
|
-- - FindPlatformMonitorForRect() [Internal]
|
|
-- - UpdateViewportPlatformMonitor() [Internal]
|
|
-- - DestroyPlatformWindow() [Internal]
|
|
-- - DestroyPlatformWindows()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function MT.ImGuiViewport:GetDebugName()
|
|
local viewport = self
|
|
return viewport.Window and viewport.Window.Name or "n/a"
|
|
end
|
|
|
|
function MT.ImGuiPlatformIO:ClearPlatformHandlers()
|
|
self.Platform_GetClipboardTextFn = nil; self.Platform_SetClipboardTextFn = nil
|
|
self.Platform_ClipboardUserData = nil
|
|
self.Platform_OpenInShellFn = nil; self.Platform_OpenInShellUserData = nil
|
|
self.Platform_SetImeDataFn = nil; self.Platform_ImeUserData = nil
|
|
self.Platform_CreateWindow = nil; self.Platform_DestroyWindow = nil; self.Platform_ShowWindow = nil
|
|
self.Platform_SetWindowPos = nil; self.Platform_SetWindowSize = nil
|
|
self.Platform_GetWindowPos = nil; self.Platform_GetWindowSize = nil; self.Platform_GetWindowFramebufferScale = nil
|
|
self.Platform_SetWindowFocus = nil; self.Platform_GetWindowFocus = nil; self.Platform_GetWindowMinimized = nil
|
|
self.Platform_SetWindowTitle = nil; self.Platform_SetWindowAlpha = nil; self.Platform_UpdateWindow = nil
|
|
self.Platform_RenderWindow = nil; self.Platform_SwapBuffers = nil; self.Platform_GetWindowDpiScale = nil
|
|
self.Platform_OnChangedViewport = nil; self.Platform_GetWindowWorkAreaInsets = nil; self.Platform_CreateVkSurface = nil
|
|
end
|
|
|
|
function MT.ImGuiPlatformIO:ClearRendererHandlers()
|
|
self.Renderer_TextureMaxWidth = 0; self.Renderer_TextureMaxHeight = 0
|
|
self.Renderer_RenderState = nil
|
|
self.Renderer_CreateWindow = nil; self.Renderer_DestroyWindow = nil
|
|
self.Renderer_SetWindowSize = nil
|
|
self.Renderer_RenderWindow = nil; self.Renderer_SwapBuffers = nil
|
|
end
|
|
|
|
--- @return ImGuiViewport
|
|
function ImGui.GetMainViewport()
|
|
local g = GImGui
|
|
|
|
return g.Viewports[1]
|
|
end
|
|
|
|
-- FIXME: This leaks access to viewports not listed in PlatformIO.Viewports[]. Problematic? (#4236)
|
|
--- @param viewport_id ImGuiID
|
|
--- @return ImGuiViewport?
|
|
function ImGui.FindViewportByID(viewport_id)
|
|
local g = GImGui
|
|
for _, viewport in g.Viewports:iter() do
|
|
if viewport.ID == viewport_id then
|
|
return viewport
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- @param current_window? ImGuiWindow
|
|
--- @param viewport? ImGuiViewportP
|
|
function ImGui.SetCurrentViewport(current_window, viewport)
|
|
local g = GImGui
|
|
|
|
if viewport then
|
|
viewport.LastFrameActive = g.FrameCount
|
|
end
|
|
if g.CurrentViewport == viewport then
|
|
return
|
|
end
|
|
|
|
g.CurrentDpiScale = viewport and viewport.DpiScale or 1.0
|
|
g.CurrentViewport = viewport
|
|
IM_ASSERT(g.CurrentDpiScale > 0.0 and g.CurrentDpiScale < 99.0) -- Typical correct values would be between 1.0f and 4.0f
|
|
-- IMGUI_DEBUG_LOG_VIEWPORT("[viewport] SetCurrentViewport changed '%s' 0x%08X", current_window and current_window.Name or "NULL", viewport and viewport.ID or 0)
|
|
|
|
if g.IO.ConfigDpiScaleFonts then
|
|
g.Style.FontScaleDpi = g.CurrentDpiScale
|
|
end
|
|
|
|
-- Notify platform layer of viewport changes
|
|
-- FIXME-DPI: This is only currently used for experimenting with handling of multiple DPI
|
|
if g.CurrentViewport and g.PlatformIO.Platform_OnChangedViewport then
|
|
g.PlatformIO.Platform_OnChangedViewport(g.CurrentViewport)
|
|
end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param viewport ImGuiViewportP
|
|
function ImGui.SetWindowViewport(window, viewport)
|
|
-- Abandon viewport
|
|
if window.ViewportOwned and window.Viewport.Window == window then
|
|
window.Viewport.Size = ImVec2(0.0, 0.0)
|
|
end
|
|
|
|
window.Viewport = viewport
|
|
window.ViewportId = viewport.ID
|
|
window.ViewportOwned = (viewport.Window == window)
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @return bool
|
|
function ImGui.GetWindowAlwaysWantOwnViewport(window)
|
|
-- Tooltips and menus are not automatically forced into their own viewport when the NoMerge flag is set, however the multiplication of viewports makes them more likely to protrude and create their own.
|
|
local g = GImGui
|
|
if g.IO.ConfigViewportsNoAutoMerge or (bit32.band(window.WindowClass.ViewportFlagsOverrideSet, ImGuiViewportFlags.NoAutoMerge) ~= 0) then
|
|
if bit32.band(g.ConfigFlagsCurrFrame, ImGuiConfigFlags.ViewportsEnable) ~= 0 then
|
|
if not window.DockIsActive then
|
|
if bit32.band(window.Flags, bit32.bor(ImGuiWindowFlags.ChildWindow, ImGuiWindowFlags.ChildMenu, ImGuiWindowFlags.Tooltip)) == 0 then
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Popup) == 0 or bit32.band(window.Flags, ImGuiWindowFlags.Modal) ~= 0 then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Heuristic, see #8948: depends on how backends handle OS-level parenting.
|
|
-- Due to how parent viewport stack is layed out, note that IsViewportAbove(a,b) isn't always the same as !IsViewportAbove(b,a).
|
|
--- @param potential_above ImGuiViewportP
|
|
--- @param potential_below ImGuiViewportP
|
|
--- @return bool
|
|
function ImGui.IsViewportAbove(potential_above, potential_below)
|
|
-- If ImGuiBackendFlags.HasParentViewport if set, ->ParentViewport chain should be accurate.
|
|
local g = GImGui
|
|
if bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.HasParentViewport) ~= 0 then
|
|
local v = potential_above --[[@as ImGuiViewport]]
|
|
while v ~= nil and v.ParentViewport do
|
|
if v.ParentViewport == potential_below then
|
|
return true
|
|
end
|
|
v = v.ParentViewport
|
|
end
|
|
else
|
|
if potential_above.ParentViewport == potential_below then
|
|
return true
|
|
end
|
|
end
|
|
|
|
if potential_above.LastFocusedStampCount > potential_below.LastFocusedStampCount then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param viewport_dst ImGuiViewportP
|
|
function ImGui.UpdateTryMergeWindowIntoHostViewport(window, viewport_dst)
|
|
local g = GImGui
|
|
IM_ASSERT(window == window.RootWindowDockTree)
|
|
|
|
local viewport_src = window.Viewport -- Current viewport
|
|
if viewport_src == viewport_dst then
|
|
return false
|
|
end
|
|
if bit32.band(viewport_dst.Flags, ImGuiViewportFlags.CanHostOtherWindows) == 0 then
|
|
return false
|
|
end
|
|
if bit32.band(viewport_dst.Flags, ImGuiViewportFlags.IsMinimized) ~= 0 then
|
|
return false
|
|
end
|
|
if not viewport_dst:GetMainRect():Contains(window:Rect()) then
|
|
return false
|
|
end
|
|
if ImGui.GetWindowAlwaysWantOwnViewport(window) then
|
|
return false
|
|
end
|
|
|
|
for _, viewport_obstructing in g.Viewports:iter() do
|
|
repeat
|
|
if viewport_obstructing == viewport_src or viewport_obstructing == viewport_dst or not viewport_obstructing.PlatformWindowCreated then
|
|
break
|
|
end
|
|
|
|
if viewport_obstructing:GetMainRect():Overlaps(window:Rect()) then
|
|
if ImGui.IsViewportAbove(viewport_obstructing, viewport_dst) then
|
|
if viewport_src == nil or ImGui.IsViewportAbove(viewport_src, viewport_obstructing) then
|
|
return false -- viewport_obstructing is between viewport_src and viewport_dst -> Cannot merge.
|
|
end
|
|
end
|
|
end
|
|
|
|
until true
|
|
end
|
|
|
|
-- Move to the existing viewport, Move child/hosted windows as well (FIXME-OPT: iterate child)
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Window '%s' merge into Viewport 0X%08X", window.Name, viewport_dst.ID)
|
|
if window.ViewportOwned then
|
|
for n = 1, g.Windows.Size do
|
|
if g.Windows.Data[n].Viewport == viewport_src then
|
|
ImGui.SetWindowViewport(g.Windows.Data[n], viewport_dst)
|
|
end
|
|
end
|
|
end
|
|
|
|
ImGui.SetWindowViewport(window, viewport_dst)
|
|
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoBringToFrontOnFocus) == 0 then
|
|
ImGui.BringWindowToDisplayFront(window)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- FIXME: handle 0 to N host viewports
|
|
--- @param window ImGuiWindow
|
|
--- @return bool
|
|
function ImGui.UpdateTryMergeWindowIntoHostViewports(window)
|
|
local g = GImGui
|
|
return ImGui.UpdateTryMergeWindowIntoHostViewport(window, g.Viewports.Data[1])
|
|
end
|
|
|
|
-- Translate Dear ImGui windows when a Host Viewport has been moved
|
|
-- (This additionally keeps windows at the same place when ImGuiConfigFlags.ViewportsEnable is toggled!)
|
|
--- @param viewport ImGuiViewportP
|
|
--- @param old_pos ImVec2
|
|
--- @param new_pos ImVec2
|
|
--- @param old_size ImVec2
|
|
--- @param new_size ImVec2
|
|
function ImGui.TranslateWindowsInViewport(viewport, old_pos, new_pos, old_size, new_size)
|
|
local g = GImGui
|
|
-- IMGUI_DEBUG_LOG_VIEWPORT("[viewport] TranslateWindowsInViewport 0x%08X", viewport.ID)
|
|
IM_ASSERT(viewport.Window == nil and (bit32.band(viewport.Flags, ImGuiViewportFlags.CanHostOtherWindows) ~= 0))
|
|
|
|
-- 1) We test if ImGuiConfigFlags.ViewportsEnable was just toggled, which allows us to conveniently
|
|
-- translate imgui windows from OS-window-local to absolute coordinates or vice-versa.
|
|
-- 2) If it's not going to fit into the new size, keep it at same absolute position.
|
|
-- One problem with this is that most Win32 applications doesn't update their render while dragging,
|
|
-- and so the window will appear to teleport when releasing the mouse.
|
|
local translate_all_windows = (bit32.band(g.ConfigFlagsCurrFrame, ImGuiConfigFlags.ViewportsEnable) ~= bit32.band(g.ConfigFlagsLastFrame, ImGuiConfigFlags.ViewportsEnable))
|
|
local test_still_fit_rect = ImRect(old_pos, old_pos + old_size)
|
|
local delta_pos = new_pos - old_pos
|
|
|
|
for _, window in g.Windows:iter() do -- FIXME-OPT
|
|
if translate_all_windows or (window.Viewport == viewport and (old_size == new_size or test_still_fit_rect:Contains(window:Rect()))) then
|
|
ImGui.TranslateWindow(window, delta_pos)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If the backend doesn't support ImGuiBackendFlags.HasMouseHoveredViewport or doesn't honor ImGuiViewportFlags.NoInputs for it, we do a search ourselves.
|
|
-- A) It won't take account of the possibility that non-imgui windows may be in-between our dragged window and our target window.
|
|
-- B) It requires Platform_GetWindowFocus to be implemented by backend.
|
|
--- @param mouse_platform_pos ImVec2
|
|
--- @return ImGuiViewportP?
|
|
function ImGui.FindHoveredViewportFromPlatformWindowStack(mouse_platform_pos)
|
|
local g = GImGui
|
|
local best_candidate = nil
|
|
for _, viewport in g.Viewports:iter() do
|
|
if bit32.band(viewport.Flags, bit32.bor(ImGuiViewportFlags.NoInputs, ImGuiViewportFlags.IsMinimized)) == 0 and viewport:GetMainRect():Contains(mouse_platform_pos) then
|
|
if best_candidate == nil or best_candidate.LastFocusedStampCount < viewport.LastFocusedStampCount then
|
|
if viewport.PlatformWindowCreated then
|
|
best_candidate = viewport
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return best_candidate
|
|
end
|
|
|
|
-- Update viewports and monitor infos
|
|
-- Note that this is running even if 'ImGuiConfigFlags.ViewportsEnable' is not set, in order to clear unused viewports (if any) and update monitor info.
|
|
function ImGui.UpdateViewportsNewFrame()
|
|
local g = GImGui
|
|
IM_ASSERT(g.PlatformIO.Viewports.Size <= g.Viewports.Size)
|
|
|
|
-- Update Minimized status (we need it first in order to decide if we'll apply Pos/Size of the main viewport)
|
|
-- Update Focused status
|
|
local viewports_enabled = (bit32.band(g.ConfigFlagsCurrFrame, ImGuiConfigFlags.ViewportsEnable) ~= 0)
|
|
if viewports_enabled then
|
|
local focused_viewport = nil
|
|
|
|
for i = 1, g.Viewports.Size do
|
|
local viewport = g.Viewports.Data[i]
|
|
local platform_funcs_available = viewport.PlatformWindowCreated
|
|
|
|
if g.PlatformIO.Platform_GetWindowMinimized and platform_funcs_available then
|
|
local is_minimized = g.PlatformIO.Platform_GetWindowMinimized(viewport)
|
|
if is_minimized then
|
|
viewport.Flags = bit32.bor(viewport.Flags, ImGuiViewportFlags.IsMinimized)
|
|
else
|
|
viewport.Flags = bit32.band(viewport.Flags, bit32.bnot(ImGuiViewportFlags.IsMinimized))
|
|
end
|
|
end
|
|
|
|
-- Update our implicit z-order knowledge of platform windows, which is used when the backend cannot provide io.MouseHoveredViewport.
|
|
-- When setting Platform_GetWindowFocus, it is expected that the platform backend can handle calls without crashing if it doesn't have data stored.
|
|
if g.PlatformIO.Platform_GetWindowFocus and platform_funcs_available then
|
|
local is_focused = g.PlatformIO.Platform_GetWindowFocus(viewport)
|
|
if is_focused then
|
|
viewport.Flags = bit32.bor(viewport.Flags, ImGuiViewportFlags.IsFocused)
|
|
else
|
|
viewport.Flags = bit32.band(viewport.Flags, bit32.bnot(ImGuiViewportFlags.IsFocused))
|
|
end
|
|
if is_focused then
|
|
focused_viewport = viewport
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Focused viewport has changed?
|
|
if focused_viewport and g.PlatformLastFocusedViewportId ~= focused_viewport.ID then
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Focused viewport changed %08X -> %08X '%s', attempting to apply our focus.", g.PlatformLastFocusedViewportId, focused_viewport.ID, focused_viewport.Window and focused_viewport.Window.Name or "n/a")
|
|
local prev_focused_viewport = ImGui.FindViewportByID(g.PlatformLastFocusedViewportId)
|
|
local prev_focused_has_been_destroyed = (prev_focused_viewport == nil) or (not prev_focused_viewport.PlatformWindowCreated)
|
|
|
|
-- Store a tag so we can infer z-order easily from all our windows
|
|
-- We compare PlatformLastFocusedViewportId so newly created viewports with _NoFocusOnAppearing flag
|
|
-- will keep the front most stamp instead of losing it back to their parent viewport.
|
|
if focused_viewport.LastFocusedStampCount ~= g.ViewportFocusedStampCount then
|
|
g.ViewportFocusedStampCount = g.ViewportFocusedStampCount + 1
|
|
focused_viewport.LastFocusedStampCount = g.ViewportFocusedStampCount
|
|
end
|
|
g.PlatformLastFocusedViewportId = focused_viewport.ID
|
|
|
|
-- Focus associated dear imgui window
|
|
-- - if focus didn't happen with a click within imgui boundaries, e.g. Clicking platform title bar. (#6299)
|
|
-- - if focus didn't happen because we destroyed another window (#6462)
|
|
-- FIXME: perhaps 'FocusTopMostWindowUnderOne()' can handle the 'focused_window.Window ~= nil' case as well.
|
|
local apply_imgui_focus_on_focused_viewport = not ImGui.IsAnyMouseDown() and not prev_focused_has_been_destroyed
|
|
if apply_imgui_focus_on_focused_viewport and g.IO.ConfigViewportsPlatformFocusSetsImGuiFocus then
|
|
focused_viewport.LastFocusedHadNavWindow = focused_viewport.LastFocusedHadNavWindow or (g.NavWindow ~= nil and g.NavWindow.Viewport == focused_viewport) -- Update so a window changing viewport won't lose focus.
|
|
|
|
local focus_request_flags = bit32.bor(ImGuiFocusRequestFlags.UnlessBelowModal, ImGuiFocusRequestFlags.RestoreFocusedChild)
|
|
if focused_viewport.Window ~= nil then
|
|
ImGui.FocusWindow(focused_viewport.Window, focus_request_flags)
|
|
elseif focused_viewport.LastFocusedHadNavWindow then
|
|
ImGui.FocusTopMostWindowUnderOne(nil, nil, focused_viewport, focus_request_flags) -- Focus top most in viewport
|
|
else
|
|
ImGui.FocusWindow(nil, focus_request_flags) -- No window had focus last time viewport was focused
|
|
end
|
|
end
|
|
end
|
|
|
|
if focused_viewport then
|
|
focused_viewport.LastFocusedHadNavWindow = (g.NavWindow ~= nil) and (g.NavWindow.Viewport == focused_viewport)
|
|
end
|
|
end
|
|
|
|
-- Create/update main viewport with current platform position.
|
|
-- FIXME-VIEWPORT: Size is driven by backend/user code for backward-compatibility but we should aim to make this more consistent.
|
|
local main_viewport = g.Viewports.Data[1]
|
|
IM_ASSERT(main_viewport.ID == IMGUI_VIEWPORT_DEFAULT_ID)
|
|
IM_ASSERT(main_viewport.Window == nil)
|
|
|
|
local main_viewport_pos
|
|
if viewports_enabled then
|
|
main_viewport_pos = g.PlatformIO.Platform_GetWindowPos(main_viewport)
|
|
else
|
|
main_viewport_pos = ImVec2(0.0, 0.0)
|
|
end
|
|
local main_viewport_size = g.IO.DisplaySize
|
|
local main_viewport_framebuffer_scale = g.IO.DisplayFramebufferScale
|
|
|
|
if viewports_enabled and (bit32.band(main_viewport.Flags, ImGuiViewportFlags.IsMinimized) ~= 0) then
|
|
ImVec2_Copy(main_viewport_pos, main_viewport.Pos) -- Preserve last pos/size when minimized (FIXME: We don't do the same for Size outside of the viewport path)
|
|
ImVec2_Copy(main_viewport_size, main_viewport.Size)
|
|
main_viewport_framebuffer_scale = main_viewport.FramebufferScale
|
|
end
|
|
|
|
ImGui.AddUpdateViewport(nil, IMGUI_VIEWPORT_DEFAULT_ID, main_viewport_pos, main_viewport_size, bit32.bor(ImGuiViewportFlags.OwnedByApp, ImGuiViewportFlags.CanHostOtherWindows))
|
|
|
|
local memory_compact_start_time = (g.GcCompactAll or g.IO.ConfigMemoryCompactTimer < 0.0) and FLT_MAX or (g.Time - g.IO.ConfigMemoryCompactTimer)
|
|
|
|
g.CurrentDpiScale = 0.0
|
|
g.CurrentViewport = nil
|
|
g.MouseViewport = nil
|
|
|
|
local n = 1
|
|
while n <= g.Viewports.Size do
|
|
local viewport = g.Viewports.Data[n]
|
|
viewport.Idx = n
|
|
|
|
-- Erase unused viewports
|
|
if n > 1 and viewport.LastFrameActive < g.FrameCount - 2 then
|
|
ImGui.DestroyViewport(viewport)
|
|
-- n stays the same because we removed an element
|
|
else
|
|
local platform_funcs_available = viewport.PlatformWindowCreated
|
|
|
|
if viewports_enabled then
|
|
-- Update Position and Size (from Platform Window to ImGui) if requested.
|
|
-- We do it early in the frame instead of waiting for UpdatePlatformWindows() to avoid a frame of lag when moving/resizing using OS facilities.
|
|
if (bit32.band(viewport.Flags, ImGuiViewportFlags.IsMinimized) == 0) and platform_funcs_available then
|
|
-- Viewport->WorkPos and WorkSize will be updated below
|
|
if viewport.PlatformRequestMove then
|
|
ImVec2_Copy(viewport.Pos, g.PlatformIO.Platform_GetWindowPos(viewport))
|
|
ImVec2_Copy(viewport.LastPlatformPos, viewport.Pos)
|
|
end
|
|
if viewport.PlatformRequestResize then
|
|
viewport.Size = g.PlatformIO.Platform_GetWindowSize(viewport)
|
|
ImVec2_Copy(viewport.LastPlatformSize, viewport.Size)
|
|
end
|
|
if g.PlatformIO.Platform_GetWindowFramebufferScale ~= nil then
|
|
viewport.FramebufferScale = g.PlatformIO.Platform_GetWindowFramebufferScale(viewport)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Update/copy monitor info
|
|
ImGui.UpdateViewportPlatformMonitor(viewport)
|
|
|
|
-- Lock down space taken by menu bars and status bars + query initial insets from backend
|
|
-- Setup initial value for functions like BeginMainMenuBar(), DockSpaceOverViewport() etc.
|
|
viewport.WorkInsetMin = viewport.BuildWorkInsetMin
|
|
viewport.WorkInsetMax = viewport.BuildWorkInsetMax
|
|
viewport.BuildWorkInsetMin = ImVec2(0.0, 0.0)
|
|
viewport.BuildWorkInsetMax = ImVec2(0.0, 0.0)
|
|
|
|
if g.PlatformIO.Platform_GetWindowWorkAreaInsets ~= nil and platform_funcs_available then
|
|
local insets = g.PlatformIO.Platform_GetWindowWorkAreaInsets(viewport)
|
|
IM_ASSERT(insets.x >= 0.0 and insets.y >= 0.0 and insets.z >= 0.0 and insets.w >= 0.0)
|
|
viewport.BuildWorkInsetMin = ImVec2(insets.x, insets.y)
|
|
viewport.BuildWorkInsetMax = ImVec2(insets.z, insets.w)
|
|
end
|
|
|
|
viewport:UpdateWorkRect()
|
|
|
|
-- Garbage collect transient buffers of recently BG/FG drawlists
|
|
for dl_n = 1, 2 do -- IM_COUNTOF(viewport->BgFgDrawLists)
|
|
if viewport.BgFgDrawListsLastTimeActive[dl_n] < memory_compact_start_time and viewport.BgFgDrawLists[dl_n] ~= nil then
|
|
IM_DELETE(viewport.BgFgDrawLists, dl_n)
|
|
end
|
|
end
|
|
|
|
-- Reset alpha every frame. Users of transparency (docking) needs to request a lower alpha back.
|
|
viewport.Alpha = 1.0
|
|
|
|
-- Translate Dear ImGui windows when a Host Viewport has been moved
|
|
-- (This additionally keeps windows at the same place when ImGuiConfigFlags.ViewportsEnable is toggled!)
|
|
local viewport_delta_pos = viewport.Pos - viewport.LastPos
|
|
if (bit32.band(viewport.Flags, ImGuiViewportFlags.CanHostOtherWindows) ~= 0) and (viewport_delta_pos.x ~= 0.0 or viewport_delta_pos.y ~= 0.0) then
|
|
ImGui.TranslateWindowsInViewport(viewport, viewport.LastPos, viewport.Pos, viewport.LastSize, viewport.Size)
|
|
end
|
|
|
|
-- Update DPI scale
|
|
local new_dpi_scale
|
|
if g.PlatformIO.Platform_GetWindowDpiScale and platform_funcs_available then
|
|
new_dpi_scale = g.PlatformIO.Platform_GetWindowDpiScale(viewport)
|
|
elseif viewport.PlatformMonitor ~= -1 then
|
|
new_dpi_scale = g.PlatformIO.Monitors.Data[viewport.PlatformMonitor].DpiScale
|
|
else
|
|
new_dpi_scale = (viewport.DpiScale ~= 0.0) and viewport.DpiScale or 1.0
|
|
end
|
|
|
|
IM_ASSERT(new_dpi_scale > 0.0 and new_dpi_scale < 99.0) -- Typical correct values would be between 1.0f and 4.0f
|
|
|
|
if viewport.DpiScale ~= 0.0 and new_dpi_scale ~= viewport.DpiScale then
|
|
local scale_factor = new_dpi_scale / viewport.DpiScale
|
|
if g.IO.ConfigDpiScaleViewports then
|
|
ImGui.ScaleWindowsInViewport(viewport, scale_factor)
|
|
end
|
|
-- if viewport == ImGui.GetMainViewport() then
|
|
-- g.PlatformInterface.SetWindowSize(viewport, viewport.Size * scale_factor)
|
|
-- end
|
|
|
|
-- Scale our window moving pivot so that the window will rescale roughly around the mouse position.
|
|
-- FIXME-VIEWPORT: This currently creates a resizing feedback loop when a window is straddling a DPI transition border.
|
|
-- (Minor: since our sizes do not perfectly linearly scale, deferring the click offset scale until we know the actual window scale ratio may get us slightly more precise mouse positioning.)
|
|
-- if g.MovingWindow ~= nil and g.MovingWindow.Viewport == viewport then
|
|
-- g.ActiveIdClickOffset = ImTrunc(g.ActiveIdClickOffset * scale_factor)
|
|
-- end
|
|
end
|
|
|
|
viewport.DpiScale = new_dpi_scale
|
|
n = n + 1
|
|
end
|
|
end
|
|
|
|
-- Update fallback monitor
|
|
g.PlatformMonitorsFullWorkRect = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX)
|
|
|
|
if g.PlatformIO.Monitors.Size == 0 then
|
|
local monitor = g.FallbackMonitor
|
|
ImVec2_Copy(monitor.MainPos, main_viewport.Pos)
|
|
ImVec2_Copy(monitor.MainSize, main_viewport.Size)
|
|
ImVec2_Copy(monitor.WorkPos, main_viewport.WorkPos)
|
|
ImVec2_Copy(monitor.WorkSize, main_viewport.WorkSize)
|
|
monitor.DpiScale = main_viewport.DpiScale
|
|
|
|
g.PlatformMonitorsFullWorkRect:Add(monitor.WorkPos)
|
|
g.PlatformMonitorsFullWorkRect:Add(monitor.WorkPos + monitor.WorkSize)
|
|
else
|
|
g.FallbackMonitor = g.PlatformIO.Monitors.Data[1]
|
|
end
|
|
|
|
for i = 1, g.PlatformIO.Monitors.Size do
|
|
local monitor = g.PlatformIO.Monitors.Data[i]
|
|
g.PlatformMonitorsFullWorkRect:Add(monitor.WorkPos)
|
|
g.PlatformMonitorsFullWorkRect:Add(monitor.WorkPos + monitor.WorkSize)
|
|
end
|
|
|
|
if not viewports_enabled then
|
|
g.MouseViewport = main_viewport
|
|
return
|
|
end
|
|
|
|
-- Mouse handling: decide on the actual mouse viewport for this frame between the active/focused viewport and the hovered viewport.
|
|
-- Note that 'viewport_hovered' should skip over any viewport that has the ImGuiViewportFlags.NoInputs flags set.
|
|
local viewport_hovered = nil
|
|
|
|
if bit32.band(g.IO.BackendFlags, ImGuiBackendFlags.HasMouseHoveredViewport) ~= 0 then
|
|
viewport_hovered = g.IO.MouseHoveredViewport and ImGui.FindViewportByID(g.IO.MouseHoveredViewport) or nil
|
|
if viewport_hovered and (bit32.band(viewport_hovered.Flags, ImGuiViewportFlags.NoInputs) ~= 0) then
|
|
viewport_hovered = ImGui.FindHoveredViewportFromPlatformWindowStack(g.IO.MousePos) -- Backend failed to handle _NoInputs viewport: revert to our fallback.
|
|
end
|
|
else
|
|
-- If the backend doesn't know how to honor ImGuiViewportFlags.NoInputs, we do a search ourselves. Note that this search:
|
|
-- A) won't take account of the possibility that non-imgui windows may be in-between our dragged window and our target window.
|
|
-- B) won't take account of how the backend apply parent<>child relationship to secondary viewports, which affects their Z order.
|
|
-- C) uses LastFocusedStampCount as a flawed replacement for the last time a window was focused (we could/should fix that by introducing Focus functions in PlatformIO)
|
|
viewport_hovered = ImGui.FindHoveredViewportFromPlatformWindowStack(g.IO.MousePos)
|
|
end
|
|
|
|
if viewport_hovered ~= nil then
|
|
g.MouseLastHoveredViewport = viewport_hovered
|
|
elseif g.MouseLastHoveredViewport == nil then
|
|
g.MouseLastHoveredViewport = g.Viewports.Data[1]
|
|
end
|
|
|
|
-- Update mouse reference viewport
|
|
-- (when moving a window we aim at its viewport, but this will be overwritten below if we go in drag and drop mode)
|
|
-- (MovingViewport->Viewport will be nil in the rare situation where the window disappared while moving, set UpdateMouseMovingWindowNewFrame() for details)
|
|
if g.MovingWindow and g.MovingWindow.Viewport then
|
|
g.MouseViewport = g.MovingWindow.Viewport
|
|
else
|
|
g.MouseViewport = g.MouseLastHoveredViewport
|
|
end
|
|
|
|
-- When dragging something, always refer to the last hovered viewport.
|
|
-- - when releasing a moving window we will revert to aiming behind (at viewport_hovered)
|
|
-- - when we are between viewports, our dragged preview will tend to show in the last viewport _even_ if we don't have tooltips in their viewports (when lacking monitor info)
|
|
-- - consider the case of holding on a menu item to browse child menus: even thou a mouse button is held, there's no active id because menu items only react on mouse release.
|
|
-- FIXME-VIEWPORT: This is essentially broken, when ImGuiBackendFlags.HasMouseHoveredViewport is set we want to trust when viewport_hovered==nil and use that.
|
|
local is_mouse_dragging_with_an_expected_destination = g.DragDropActive
|
|
if is_mouse_dragging_with_an_expected_destination and viewport_hovered == nil then
|
|
viewport_hovered = g.MouseLastHoveredViewport
|
|
end
|
|
|
|
if is_mouse_dragging_with_an_expected_destination or g.ActiveId == 0 or not ImGui.IsAnyMouseDown() then
|
|
if viewport_hovered ~= nil and viewport_hovered ~= g.MouseViewport and (bit32.band(viewport_hovered.Flags, ImGuiViewportFlags.NoInputs) == 0) then
|
|
g.MouseViewport = viewport_hovered
|
|
end
|
|
end
|
|
|
|
IM_ASSERT(g.MouseViewport ~= nil)
|
|
end
|
|
|
|
-- Update user-facing viewport list (g.Viewports -> g.PlatformIO.Viewports after filtering out some)
|
|
function ImGui.UpdateViewportsEndFrame()
|
|
local g = GImGui
|
|
g.PlatformIO.Viewports:resize(0)
|
|
|
|
for i = 1, g.Viewports.Size do
|
|
repeat
|
|
local viewport = g.Viewports.Data[i]
|
|
ImVec2_Copy(viewport.LastPos, ImVec2(viewport.Pos.x, viewport.Pos.y))
|
|
ImVec2_Copy(viewport.LastSize, ImVec2(viewport.Size.x, viewport.Size.y))
|
|
|
|
if (viewport.LastFrameActive < g.FrameCount or viewport.Size.x <= 0.0 or viewport.Size.y <= 0.0) and (i > 1) then -- Always include main viewport in the list
|
|
break
|
|
end
|
|
if viewport.Window and not ImGui.IsWindowActiveAndVisible(viewport.Window) then
|
|
break
|
|
end
|
|
if i > 1 then
|
|
IM_ASSERT(viewport.Window ~= nil)
|
|
end
|
|
g.PlatformIO.Viewports:push_back(viewport)
|
|
|
|
until true
|
|
end
|
|
|
|
g.Viewports.Data[1]:ClearRequestFlags() -- Clear main viewport flags because UpdatePlatformWindows() won't do it and may not even be called
|
|
end
|
|
|
|
-- FIXME: We should ideally refactor the system to call this every frame (we currently don't)
|
|
--- @param window? ImGuiWindow
|
|
--- @param id ImGuiID
|
|
--- @param pos ImVec2
|
|
--- @param size ImVec2
|
|
--- @param flags ImGuiViewportFlags
|
|
--- @return ImGuiViewportP
|
|
function ImGui.AddUpdateViewport(window, id, pos, size, flags)
|
|
local g = GImGui
|
|
IM_ASSERT(id ~= 0)
|
|
|
|
flags = bit32.bor(flags, ImGuiViewportFlags.IsPlatformWindow)
|
|
if window ~= nil then
|
|
local window_can_use_inputs = (bit32.band(window.Flags, bit32.bor(ImGuiWindowFlags.NoMouseInputs, ImGuiWindowFlags.NoNavInputs)) == 0)
|
|
if g.MovingWindow and g.MovingWindow.RootWindowDockTree == window then
|
|
flags = bit32.bor(flags, ImGuiViewportFlags.NoInputs, ImGuiViewportFlags.NoFocusOnAppearing)
|
|
end
|
|
if not window_can_use_inputs then
|
|
flags = bit32.bor(flags, ImGuiViewportFlags.NoInputs)
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.NoFocusOnAppearing) ~= 0 then
|
|
flags = bit32.bor(flags, ImGuiViewportFlags.NoFocusOnAppearing)
|
|
end
|
|
end
|
|
|
|
local viewport = ImGui.FindViewportByID(id)
|
|
if viewport then
|
|
--- @cast viewport ImGuiViewportP
|
|
|
|
-- Always update for main viewport as we are already pulling correct platform pos/size (see #4900)
|
|
local prev_pos = ImVec2(viewport.Pos.x, viewport.Pos.y)
|
|
local prev_size = ImVec2(viewport.Size.x, viewport.Size.y)
|
|
if not viewport.PlatformRequestMove or viewport.ID == IMGUI_VIEWPORT_DEFAULT_ID then
|
|
ImVec2_Copy(viewport.Pos, pos)
|
|
end
|
|
if not viewport.PlatformRequestResize or viewport.ID == IMGUI_VIEWPORT_DEFAULT_ID then
|
|
ImVec2_Copy(viewport.Size, size)
|
|
end
|
|
viewport.Flags = bit32.bor(flags, bit32.band(viewport.Flags, bit32.bor(ImGuiViewportFlags.IsMinimized, ImGuiViewportFlags.IsFocused))) -- Preserve existing flags
|
|
if prev_pos.x ~= viewport.Pos.x or prev_pos.y ~= viewport.Pos.y or prev_size.x ~= viewport.Size.x or prev_size.y ~= viewport.Size.y then
|
|
ImGui.UpdateViewportPlatformMonitor(viewport)
|
|
end
|
|
else
|
|
-- New viewport
|
|
viewport = ImGuiViewportP()
|
|
viewport.ID = id
|
|
viewport.Idx = g.Viewports.Size
|
|
ImVec2_Copy(viewport.Pos, pos)
|
|
ImVec2_Copy(viewport.LastPos, pos)
|
|
ImVec2_Copy(viewport.Size, size)
|
|
ImVec2_Copy(viewport.LastSize, size)
|
|
viewport.Flags = flags
|
|
ImGui.UpdateViewportPlatformMonitor(viewport)
|
|
g.Viewports:push_back(viewport)
|
|
g.ViewportCreatedCount = g.ViewportCreatedCount + 1
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Add Viewport %08X '%s'", id, window and window.Name or "<NULL>")
|
|
|
|
-- We assume the window becomes front-most (even when ImGuiViewportFlags.NoFocusOnAppearing is used).
|
|
-- This is useful for our platform z-order heuristic when io.MouseHoveredViewport is not available.
|
|
g.ViewportFocusedStampCount = g.ViewportFocusedStampCount + 1
|
|
viewport.LastFocusedStampCount = g.ViewportFocusedStampCount
|
|
|
|
-- We normally setup for all viewports in NewFrame() but here need to handle the mid-frame creation of a new viewport.
|
|
-- We need to extend the fullscreen clip rect so the OverlayDrawList clip is correct for that the first frame
|
|
g.DrawListSharedData.ClipRectFullscreen.x = ImMin(g.DrawListSharedData.ClipRectFullscreen.x, viewport.Pos.x)
|
|
g.DrawListSharedData.ClipRectFullscreen.y = ImMin(g.DrawListSharedData.ClipRectFullscreen.y, viewport.Pos.y)
|
|
g.DrawListSharedData.ClipRectFullscreen.z = ImMax(g.DrawListSharedData.ClipRectFullscreen.z, viewport.Pos.x + viewport.Size.x)
|
|
g.DrawListSharedData.ClipRectFullscreen.w = ImMax(g.DrawListSharedData.ClipRectFullscreen.w, viewport.Pos.y + viewport.Size.y)
|
|
|
|
-- Store initial DpiScale before the OS platform window creation, based on expected monitor data.
|
|
-- This is so we can select an appropriate font size on the first frame of our window lifetime
|
|
viewport.DpiScale = ImGui.GetViewportPlatformMonitor(viewport).DpiScale
|
|
end
|
|
|
|
viewport.Window = window
|
|
viewport.LastFrameActive = g.FrameCount
|
|
viewport:UpdateWorkRect()
|
|
IM_ASSERT(window == nil or viewport.ID == window.ID)
|
|
|
|
if window ~= nil then
|
|
window.ViewportOwned = true
|
|
end
|
|
|
|
return viewport
|
|
end
|
|
|
|
--- @param viewport ImGuiViewportP
|
|
function ImGui.DestroyViewport(viewport)
|
|
-- Clear references to this viewport in windows (window->ViewportId becomes the master data)
|
|
local g = GImGui
|
|
for i = 1, g.Windows.Size do
|
|
local window = g.Windows.Data[i]
|
|
if window.Viewport == viewport then
|
|
window.Viewport = nil
|
|
window.ViewportOwned = false
|
|
end
|
|
end
|
|
|
|
if viewport == g.MouseLastHoveredViewport then
|
|
g.MouseLastHoveredViewport = nil
|
|
end
|
|
|
|
-- Destroy
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Delete Viewport %08X '%s'", viewport.ID, viewport.Window and viewport.Window.Name or "n/a")
|
|
ImGui.DestroyPlatformWindow(viewport) -- In most circumstances the platform window will already be destroyed here.
|
|
IM_ASSERT(not g.PlatformIO.Viewports:contains(viewport))
|
|
IM_ASSERT(g.Viewports.Data[viewport.Idx] == viewport)
|
|
g.Viewports:erase(viewport.Idx)
|
|
-- IM_DELETE(viewport)
|
|
end
|
|
|
|
-- FIXME-VIEWPORT: This is all super messy and ought to be clarified or rewritten.
|
|
--- @param window ImGuiWindow
|
|
function ImGui.WindowSelectViewport(window)
|
|
local g = GImGui
|
|
local flags = window.Flags
|
|
window.ViewportAllowPlatformMonitorExtend = -1
|
|
|
|
-- Restore main viewport if multi-viewport is not supported by the backend
|
|
local main_viewport = ImGui.GetMainViewport() --[[@as ImGuiViewportP]]
|
|
if bit32.band(g.ConfigFlagsCurrFrame, ImGuiConfigFlags.ViewportsEnable) == 0 then
|
|
ImGui.SetWindowViewport(window, main_viewport)
|
|
return
|
|
end
|
|
window.ViewportOwned = false
|
|
|
|
-- Appearing popups reset their viewport so they can inherit again
|
|
if bit32.band(flags, bit32.bor(ImGuiWindowFlags.Popup, ImGuiWindowFlags.Tooltip)) ~= 0 and window.Appearing then
|
|
window.Viewport = nil
|
|
window.ViewportId = 0
|
|
end
|
|
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasViewport) == 0 then
|
|
-- By default inherit from parent window
|
|
if window.Viewport == nil and window.ParentWindow and (not window.ParentWindow.IsFallbackWindow or window.ParentWindow.WasActive) then
|
|
window.Viewport = window.ParentWindow.Viewport
|
|
end
|
|
|
|
-- Attempt to restore saved viewport id (= window that hasn't been activated yet), try to restore the viewport based on saved 'window->ViewportPos' restored from .ini file
|
|
if window.Viewport == nil and window.ViewportId ~= 0 then
|
|
window.Viewport = ImGui.FindViewportByID(window.ViewportId) --[[@as ImGuiViewportP]]
|
|
if window.Viewport == nil and window.ViewportPos.x ~= FLT_MAX and window.ViewportPos.y ~= FLT_MAX then
|
|
window.Viewport = ImGui.AddUpdateViewport(window, window.ID, window.ViewportPos, window.Size, ImGuiViewportFlags.None)
|
|
end
|
|
end
|
|
end
|
|
|
|
local lock_viewport = false
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasViewport) ~= 0 then
|
|
-- Code explicitly request a viewport
|
|
window.Viewport = ImGui.FindViewportByID(g.NextWindowData.ViewportId) --[[@as ImGuiViewportP]]
|
|
window.ViewportId = g.NextWindowData.ViewportId -- Store ID even if Viewport isn't resolved yet.
|
|
if window.Viewport and bit32.band(window.Flags, ImGuiWindowFlags.DockNodeHost) ~= 0 and window.Viewport.Window ~= nil then
|
|
window.Viewport.Window = window
|
|
window.Viewport.ID = window.ID
|
|
window.ViewportId = window.ID -- Overwrite ID (always owned by node)
|
|
end
|
|
lock_viewport = true
|
|
elseif bit32.band(flags, ImGuiWindowFlags.ChildWindow) ~= 0 or bit32.band(flags, ImGuiWindowFlags.ChildMenu) ~= 0 then
|
|
-- Always inherit viewport from parent window
|
|
if window.DockNode and window.DockNode.HostWindow then
|
|
IM_ASSERT(window.DockNode.HostWindow.Viewport == window.ParentWindow.Viewport)
|
|
end
|
|
window.Viewport = window.ParentWindow.Viewport
|
|
elseif window.DockNode and window.DockNode.HostWindow then
|
|
-- This covers the "always inherit viewport from parent window" case for when a window reattach to a node that was just created mid-frame
|
|
window.Viewport = window.DockNode.HostWindow.Viewport
|
|
elseif bit32.band(flags, ImGuiWindowFlags.Tooltip) ~= 0 then
|
|
window.Viewport = g.MouseViewport
|
|
elseif ImGui.GetWindowAlwaysWantOwnViewport(window) then
|
|
window.Viewport = ImGui.AddUpdateViewport(window, window.ID, window.Pos, window.Size, ImGuiViewportFlags.None)
|
|
elseif g.MovingWindow and g.MovingWindow.RootWindowDockTree == window and ImGui.IsMousePosValid() then
|
|
if window.Viewport ~= nil and window.Viewport.Window == window then
|
|
window.Viewport = ImGui.AddUpdateViewport(window, window.ID, window.Pos, window.Size, ImGuiViewportFlags.None)
|
|
end
|
|
else
|
|
-- Merge into host viewport?
|
|
-- We cannot test window->ViewportOwned as it set lower in the function.
|
|
-- Testing (g.ActiveId == 0 || g.ActiveIdAllowOverlap) to avoid merging during a short-term widget interaction. Main intent was to avoid during resize (see #4212)
|
|
local try_to_merge_into_host_viewport = (window.Viewport and window == window.Viewport.Window and (g.ActiveId == 0 or g.ActiveIdAllowOverlap))
|
|
if try_to_merge_into_host_viewport then
|
|
ImGui.UpdateTryMergeWindowIntoHostViewports(window)
|
|
end
|
|
end
|
|
|
|
-- Fallback: merge in default viewport if z-order matches, otherwise create a new viewport
|
|
if window.Viewport == nil then
|
|
--- @cast main_viewport ImGuiViewportP
|
|
if not ImGui.UpdateTryMergeWindowIntoHostViewport(window, main_viewport) then
|
|
window.Viewport = ImGui.AddUpdateViewport(window, window.ID, window.Pos, window.Size, ImGuiViewportFlags.None)
|
|
end
|
|
end
|
|
|
|
-- Mark window as allowed to protrude outside of its viewport and into the current monitor
|
|
if not lock_viewport then
|
|
if bit32.band(flags, bit32.bor(ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.Popup)) ~= 0 then
|
|
-- We need to take account of the possibility that mouse may become invalid.
|
|
-- Popups/Tooltip always set ViewportAllowPlatformMonitorExtend so GetWindowAllowedExtentRect() will return full monitor bounds.
|
|
local mouse_ref = bit32.band(flags, ImGuiWindowFlags.Tooltip) ~= 0 and g.IO.MousePos or g.BeginPopupStack.Data[g.BeginPopupStack.Size].OpenMousePos
|
|
local use_mouse_ref = not g.NavCursorVisible or not g.NavHighlightItemUnderNav or not g.NavWindow
|
|
local mouse_valid = ImGui.IsMousePosValid(mouse_ref)
|
|
if (window.Appearing or bit32.band(flags, bit32.bor(ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.ChildMenu)) ~= 0) and (not use_mouse_ref or mouse_valid) then
|
|
local pos_to_use = (use_mouse_ref and mouse_valid) and mouse_ref or ImGui.NavCalcPreferredRefPos(window.Flags)
|
|
window.ViewportAllowPlatformMonitorExtend = ImGui.FindPlatformMonitorForPos(pos_to_use)
|
|
else
|
|
window.ViewportAllowPlatformMonitorExtend = window.Viewport.PlatformMonitor
|
|
end
|
|
elseif window.Viewport and window ~= window.Viewport.Window and window.Viewport.Window and bit32.band(flags, ImGuiWindowFlags.ChildWindow) == 0 and window.DockNode == nil then
|
|
-- When called from Begin() we don't have access to a proper version of the Hidden flag yet, so we replicate this code.
|
|
local will_be_visible = (window.DockIsActive and not window.DockTabIsVisible) and false or true
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.DockNodeHost) ~= 0 and window.Viewport.LastFrameActive < g.FrameCount and will_be_visible then
|
|
-- Steal/transfer ownership
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Window '%s' steal Viewport %08X from Window '%s'", window.Name, window.Viewport.ID, window.Viewport.Window.Name)
|
|
window.Viewport.Window = window
|
|
window.Viewport.ID = window.ID
|
|
window.Viewport.LastNameHash = 0
|
|
elseif not ImGui.UpdateTryMergeWindowIntoHostViewports(window) then -- Merge?
|
|
-- New viewport
|
|
window.Viewport = ImGui.AddUpdateViewport(window, window.ID, window.Pos, window.Size, ImGuiViewportFlags.NoFocusOnAppearing)
|
|
end
|
|
elseif window.ViewportAllowPlatformMonitorExtend < 0 and bit32.band(flags, ImGuiWindowFlags.ChildWindow) == 0 then
|
|
-- Regular (non-child, non-popup) windows by default are also allowed to protrude
|
|
-- Child windows are kept contained within their parent.
|
|
window.ViewportAllowPlatformMonitorExtend = window.Viewport.PlatformMonitor
|
|
end
|
|
end
|
|
|
|
-- Update flags
|
|
window.ViewportOwned = (window == window.Viewport.Window)
|
|
window.ViewportId = window.Viewport.ID
|
|
|
|
-- If the OS window has a title bar, hide our imgui title bar
|
|
-- if window.ViewportOwned and bit32.band(window.Viewport.Flags, ImGuiViewportFlags.NoDecoration) == 0 then
|
|
-- window.Flags = bit32.bor(window.Flags, ImGuiWindowFlags.NoTitleBar)
|
|
-- end
|
|
end
|
|
|
|
--- @param window ImGuiWindow
|
|
--- @param parent_window_in_stack? ImGuiWindow
|
|
function ImGui.WindowSyncOwnedViewport(window, parent_window_in_stack)
|
|
local g = GImGui
|
|
local viewport_rect_changed = false
|
|
|
|
-- Synchronize window --> viewport in most situations
|
|
-- Synchronize viewport -> window in case the platform window has been moved or resized from the OS/WM
|
|
if window.Viewport.PlatformRequestMove then
|
|
ImVec2_Copy(window.Pos, window.Viewport.Pos)
|
|
-- ImGui.MarkIniSettingsDirty(window)
|
|
elseif window.Viewport.Pos.x ~= window.Pos.x or window.Viewport.Pos.y ~= window.Pos.y then
|
|
viewport_rect_changed = true
|
|
ImVec2_Copy(window.Viewport.Pos, window.Pos)
|
|
end
|
|
|
|
if window.Viewport.PlatformRequestResize then
|
|
ImVec2_Copy(window.Size, window.Viewport.Size)
|
|
ImVec2_Copy(window.SizeFull, window.Viewport.Size)
|
|
-- ImGui.MarkIniSettingsDirty(window)
|
|
elseif window.Viewport.Size.x ~= window.Size.x or window.Viewport.Size.y ~= window.Size.y then
|
|
viewport_rect_changed = true
|
|
ImVec2_Copy(window.Viewport.Size, window.Size)
|
|
end
|
|
window.Viewport:UpdateWorkRect()
|
|
|
|
-- The viewport may have changed monitor since the global update in UpdateViewportsNewFrame()
|
|
-- Either a SetNextWindowPos() call in the current frame or a SetWindowPos() call in the previous frame may have this effect.
|
|
if viewport_rect_changed then
|
|
ImGui.UpdateViewportPlatformMonitor(window.Viewport)
|
|
end
|
|
|
|
-- Update common viewport flags
|
|
local viewport_flags_to_clear = bit32.bor(ImGuiViewportFlags.TopMost, ImGuiViewportFlags.NoTaskBarIcon, ImGuiViewportFlags.NoDecoration, ImGuiViewportFlags.NoRendererClear)
|
|
local viewport_flags = bit32.band(window.Viewport.Flags, bit32.bnot(viewport_flags_to_clear))
|
|
local window_flags = window.Flags
|
|
local is_modal = bit32.band(window_flags, ImGuiWindowFlags.Modal) ~= 0
|
|
local is_short_lived_floating_window = bit32.band(window_flags, bit32.bor(ImGuiWindowFlags.ChildMenu, ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.Popup)) ~= 0
|
|
|
|
if bit32.band(window_flags, ImGuiWindowFlags.Tooltip) ~= 0 then
|
|
viewport_flags = bit32.bor(viewport_flags, ImGuiViewportFlags.TopMost)
|
|
end
|
|
|
|
if (g.IO.ConfigViewportsNoTaskBarIcon or is_short_lived_floating_window) and not is_modal then
|
|
viewport_flags = bit32.bor(viewport_flags, ImGuiViewportFlags.NoTaskBarIcon)
|
|
end
|
|
|
|
if g.IO.ConfigViewportsNoDecoration or is_short_lived_floating_window then
|
|
viewport_flags = bit32.bor(viewport_flags, ImGuiViewportFlags.NoDecoration)
|
|
end
|
|
|
|
-- Not correct to set modal as topmost because:
|
|
-- - Because other popups can be stacked above a modal (e.g. combo box in a modal)
|
|
-- - ImGuiViewportFlags.TopMost is currently handled different in backends: in Win32 it is "appear top most" whereas in GLFW and SDL it is "stay topmost"
|
|
-- if bit32.band(window_flags, ImGuiWindowFlags.Modal) ~= 0 then
|
|
-- viewport_flags = bit32.bor(viewport_flags, ImGuiViewportFlags.TopMost)
|
|
-- end
|
|
|
|
-- For popups and menus that may be protruding out of their parent viewport, we enable _NoFocusOnClick so that clicking on them
|
|
-- won't steal the OS focus away from their parent window (which may be reflected in OS the title bar decoration).
|
|
-- Setting _NoFocusOnClick would technically prevent us from bringing back to front in case they are being covered by an OS window from a different app,
|
|
-- but it shouldn't be much of a problem considering those are already popups that are closed when clicking elsewhere.
|
|
if is_short_lived_floating_window and not is_modal then
|
|
viewport_flags = bit32.bor(viewport_flags, ImGuiViewportFlags.NoFocusOnAppearing, ImGuiViewportFlags.NoFocusOnClick)
|
|
end
|
|
|
|
-- We can overwrite viewport flags using ImGuiWindowClass (advanced users)
|
|
if window.WindowClass.ViewportFlagsOverrideSet ~= 0 then
|
|
viewport_flags = bit32.bor(viewport_flags, window.WindowClass.ViewportFlagsOverrideSet)
|
|
end
|
|
if window.WindowClass.ViewportFlagsOverrideClear ~= 0 then
|
|
viewport_flags = bit32.band(viewport_flags, bit32.bnot(window.WindowClass.ViewportFlagsOverrideClear))
|
|
end
|
|
|
|
-- We can also tell the backend that clearing the platform window won't be necessary,
|
|
-- as our window background is filling the viewport and we have disabled BgAlpha.
|
|
-- FIXME: Work on support for per-viewport transparency (#2766)
|
|
if bit32.band(window_flags, ImGuiWindowFlags.NoBackground) == 0 then
|
|
viewport_flags = bit32.bor(viewport_flags, ImGuiViewportFlags.NoRendererClear)
|
|
end
|
|
|
|
window.Viewport.Flags = viewport_flags
|
|
|
|
-- Update parent viewport ID
|
|
-- (the !IsFallbackWindow test mimic the one done in WindowSelectViewport())
|
|
if window.WindowClass.ParentViewportId ~= 0xFFFFFFFF then
|
|
local old_parent_viewport_id = window.Viewport.ParentViewportId
|
|
window.Viewport.ParentViewportId = window.WindowClass.ParentViewportId
|
|
if window.Viewport.ParentViewportId ~= old_parent_viewport_id then
|
|
window.Viewport.ParentViewport = ImGui.FindViewportByID(window.Viewport.ParentViewportId) --[[@as ImGuiViewport]]
|
|
end
|
|
elseif bit32.band(window_flags, bit32.bor(ImGuiWindowFlags.Popup, ImGuiWindowFlags.Tooltip)) ~= 0 and parent_window_in_stack and (not parent_window_in_stack.IsFallbackWindow or parent_window_in_stack.WasActive) then
|
|
window.Viewport.ParentViewport = parent_window_in_stack.Viewport
|
|
window.Viewport.ParentViewportId = parent_window_in_stack.Viewport.ID
|
|
else
|
|
if g.IO.ConfigViewportsNoDefaultParent then
|
|
window.Viewport.ParentViewport = nil
|
|
else
|
|
window.Viewport.ParentViewport = ImGui.GetMainViewport()
|
|
end
|
|
window.Viewport.ParentViewportId = g.IO.ConfigViewportsNoDefaultParent and 0 or IMGUI_VIEWPORT_DEFAULT_ID
|
|
end
|
|
end
|
|
|
|
-- Called by user at the end of the main loop, after EndFrame()
|
|
-- This will handle the creation/update of all OS windows via function defined in the ImGuiPlatformIO api.
|
|
function ImGui.UpdatePlatformWindows()
|
|
local g = GImGui
|
|
IM_ASSERT(g.FrameCountEnded == g.FrameCount, "Forgot to call Render() or EndFrame() before UpdatePlatformWindows()?")
|
|
IM_ASSERT(g.FrameCountPlatformEnded < g.FrameCount)
|
|
g.FrameCountPlatformEnded = g.FrameCount
|
|
if bit32.band(g.ConfigFlagsCurrFrame, ImGuiConfigFlags.ViewportsEnable) == 0 then
|
|
return
|
|
end
|
|
|
|
-- Create/resize/destroy platform windows to match each active viewport.
|
|
-- Skip the main viewport (index 0), which is always fully handled by the application!
|
|
for i = 2, g.Viewports.Size do
|
|
repeat
|
|
local viewport = g.Viewports.Data[i]
|
|
|
|
-- Destroy platform window if the viewport hasn't been submitted or if it is hosting a hidden window
|
|
-- (the implicit/fallback Debug##Default window will be registering its viewport then be disabled, causing a dummy DestroyPlatformWindow to be made each frame)
|
|
local destroy_platform_window = false
|
|
destroy_platform_window = destroy_platform_window or (viewport.LastFrameActive < g.FrameCount - 1)
|
|
destroy_platform_window = destroy_platform_window or (viewport.Window and not ImGui.IsWindowActiveAndVisible(viewport.Window)) --[[@as bool]]
|
|
if destroy_platform_window then
|
|
ImGui.DestroyPlatformWindow(viewport)
|
|
break
|
|
end
|
|
|
|
-- New windows that appears directly in a new viewport won't always have a size on their first frame
|
|
if viewport.LastFrameActive < g.FrameCount or viewport.Size.x <= 0 or viewport.Size.y <= 0 then
|
|
break
|
|
end
|
|
|
|
-- Create window
|
|
local is_new_platform_window = not viewport.PlatformWindowCreated
|
|
if is_new_platform_window then
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Create Platform Window %08X '%s'", viewport.ID, viewport.Window and viewport.Window.Name or "n/a")
|
|
g.PlatformIO.Platform_CreateWindow(viewport)
|
|
if g.PlatformIO.Renderer_CreateWindow ~= nil then
|
|
g.PlatformIO.Renderer_CreateWindow(viewport)
|
|
end
|
|
g.PlatformWindowsCreatedCount = g.PlatformWindowsCreatedCount + 1
|
|
viewport.LastNameHash = 0
|
|
ImVec2_CopyV(viewport.LastPlatformPos, FLT_MAX, FLT_MAX)
|
|
ImVec2_CopyV(viewport.LastPlatformSize, FLT_MAX, FLT_MAX) -- By clearing those we'll enforce a call to Platform_SetWindowPos/Size below, before Platform_ShowWindow (FIXME: Is that necessary?)
|
|
ImVec2_Copy(viewport.LastRendererSize, viewport.Size) -- We don't need to call Renderer_SetWindowSize() as it is expected Renderer_CreateWindow() already did it.
|
|
viewport.PlatformWindowCreated = true
|
|
end
|
|
|
|
-- Apply Position and Size (from ImGui to Platform/Renderer backends)
|
|
if (viewport.LastPlatformPos.x ~= viewport.Pos.x or viewport.LastPlatformPos.y ~= viewport.Pos.y) and not viewport.PlatformRequestMove then
|
|
g.PlatformIO.Platform_SetWindowPos(viewport, viewport.Pos)
|
|
end
|
|
if (viewport.LastPlatformSize.x ~= viewport.Size.x or viewport.LastPlatformSize.y ~= viewport.Size.y) and not viewport.PlatformRequestResize then
|
|
g.PlatformIO.Platform_SetWindowSize(viewport, viewport.Size)
|
|
end
|
|
if (viewport.LastRendererSize.x ~= viewport.Size.x or viewport.LastRendererSize.y ~= viewport.Size.y) and g.PlatformIO.Renderer_SetWindowSize ~= nil then
|
|
g.PlatformIO.Renderer_SetWindowSize(viewport, viewport.Size)
|
|
end
|
|
ImVec2_Copy(viewport.LastPlatformPos, viewport.Pos)
|
|
ImVec2_Copy(viewport.LastPlatformSize, viewport.Size)
|
|
ImVec2_Copy(viewport.LastRendererSize, viewport.Size)
|
|
|
|
-- Update title bar (if it changed)
|
|
local window_for_title = GetWindowForTitleDisplay(viewport.Window)
|
|
if window_for_title ~= nil then
|
|
local title = window_for_title.Name
|
|
local title_begin = 1
|
|
local title_end = ImGui.FindRenderedTextEnd(title)
|
|
local title_hash = ImHashStr(title, title_end - title_begin)
|
|
if viewport.LastNameHash ~= title_hash then
|
|
-- This still creates new strings
|
|
g.PlatformIO.Platform_SetWindowTitle(viewport, string.sub(title, title_begin, title_end - 1))
|
|
viewport.LastNameHash = title_hash
|
|
end
|
|
end
|
|
|
|
-- Update alpha (if it changed)
|
|
if viewport.LastAlpha ~= viewport.Alpha and g.PlatformIO.Platform_SetWindowAlpha ~= nil then
|
|
g.PlatformIO.Platform_SetWindowAlpha(viewport, viewport.Alpha)
|
|
end
|
|
viewport.LastAlpha = viewport.Alpha
|
|
|
|
-- Optional, general purpose call to allow the backend to perform general book-keeping even if things haven't changed.
|
|
if g.PlatformIO.Platform_UpdateWindow ~= nil then
|
|
g.PlatformIO.Platform_UpdateWindow(viewport)
|
|
end
|
|
|
|
if is_new_platform_window then
|
|
-- On startup ensure new platform window don't steal focus (give it a few frames, as nested contents may lead to viewport being created a few frames late)
|
|
if g.FrameCount < 3 then
|
|
viewport.Flags = bit32.bor(viewport.Flags, ImGuiViewportFlags.NoFocusOnAppearing)
|
|
end
|
|
|
|
-- Show window
|
|
g.PlatformIO.Platform_ShowWindow(viewport)
|
|
end
|
|
|
|
-- Clear request flags
|
|
viewport:ClearRequestFlags()
|
|
|
|
until true
|
|
end
|
|
end
|
|
|
|
-- This is a default/basic function for performing the rendering/swap of multiple Platform Windows.
|
|
-- Custom renderers may prefer to not call this function at all, and instead iterate the publicly exposed platform data and handle rendering/sync themselves.
|
|
-- The Render/Swap functions stored in ImGuiPlatformIO are merely here to allow for this helper to exist, but you can do it yourself:
|
|
--
|
|
-- local platform_io = ImGui.GetPlatformIO()
|
|
-- for i = 2, platform_io.Viewports.Size do
|
|
-- if bit32.band(platform_io.Viewports.Data[i].Flags, ImGuiViewportFlags.IsMinimized) == 0 then
|
|
-- MyRenderFunction(platform_io.Viewports.Data[i], my_args)
|
|
-- end
|
|
-- end
|
|
-- for i = 2, platform_io.Viewports.Size do
|
|
-- if bit32.band(platform_io.Viewports.Data[i].Flags, ImGuiViewportFlags.IsMinimized) == 0 then
|
|
-- MySwapBufferFunction(platform_io.Viewports.Data[i], my_args)
|
|
-- end
|
|
-- end
|
|
--
|
|
function ImGui.RenderPlatformWindowsDefault(platform_render_arg, renderer_render_arg)
|
|
-- Skip the main viewport (index 1 in Lua), which is always fully handled by the application!
|
|
local platform_io = ImGui.GetPlatformIO()
|
|
|
|
-- First pass: render all windows
|
|
for i = 2, platform_io.Viewports.Size do
|
|
local viewport = platform_io.Viewports.Data[i]
|
|
if bit32.band(viewport.Flags, ImGuiViewportFlags.IsMinimized) == 0 then
|
|
if platform_io.Platform_RenderWindow then
|
|
platform_io.Platform_RenderWindow(viewport, platform_render_arg)
|
|
end
|
|
if platform_io.Renderer_RenderWindow then
|
|
platform_io.Renderer_RenderWindow(viewport, renderer_render_arg)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Second pass: swap buffers for all windows
|
|
for i = 2, platform_io.Viewports.Size do
|
|
local viewport = platform_io.Viewports.Data[i]
|
|
if bit32.band(viewport.Flags, ImGuiViewportFlags.IsMinimized) == 0 then
|
|
if platform_io.Platform_SwapBuffers then
|
|
platform_io.Platform_SwapBuffers(viewport, platform_render_arg)
|
|
end
|
|
if platform_io.Renderer_SwapBuffers then
|
|
platform_io.Renderer_SwapBuffers(viewport, renderer_render_arg)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param pos ImVec2
|
|
function ImGui.FindPlatformMonitorForPos(pos)
|
|
local g = GImGui
|
|
for monitor_n = 1, g.PlatformIO.Monitors.Size do
|
|
local monitor = g.PlatformIO.Monitors.Data[monitor_n]
|
|
if ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize):Contains(pos) then
|
|
return monitor_n
|
|
end
|
|
end
|
|
return -1
|
|
end
|
|
|
|
-- Search for the monitor with the largest intersection area with the given rectangle
|
|
-- We generally try to avoid searching loops but the monitor count should be very small here
|
|
-- FIXME-OPT: We could test the last monitor used for that viewport first, and early
|
|
--- @param rect ImRect
|
|
--- @return int
|
|
function ImGui.FindPlatformMonitorForRect(rect)
|
|
local g = GImGui
|
|
local monitor_count = g.PlatformIO.Monitors.Size
|
|
|
|
if monitor_count <= 1 then
|
|
return monitor_count
|
|
end
|
|
|
|
-- Use a minimum threshold of 1.0f so a zero-sized rect won't false positive, and will still find the correct monitor given its position.
|
|
-- This is necessary for tooltips which always resize down to zero at first.
|
|
local surface_threshold = math.max(rect:GetWidth() * rect:GetHeight() * 0.5, 1.0)
|
|
local best_monitor_n = 1 -- Default to the first monitor as fallback
|
|
local best_monitor_surface = 0.001
|
|
|
|
for monitor_n = 1, g.PlatformIO.Monitors.Size do
|
|
if best_monitor_surface >= surface_threshold then
|
|
break
|
|
end
|
|
|
|
local monitor = g.PlatformIO.Monitors.Data[monitor_n]
|
|
local monitor_rect = ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize)
|
|
|
|
if monitor_rect:Contains(rect) then
|
|
return monitor_n
|
|
end
|
|
|
|
local overlapping_rect = ImRect(rect.Min.x, rect.Min.y, rect.Max.x, rect.Max.y)
|
|
overlapping_rect:ClipWithFull(monitor_rect)
|
|
local overlapping_surface = overlapping_rect:GetWidth() * overlapping_rect:GetHeight()
|
|
|
|
if overlapping_surface > best_monitor_surface then
|
|
best_monitor_surface = overlapping_surface
|
|
best_monitor_n = monitor_n
|
|
end
|
|
end
|
|
|
|
return best_monitor_n
|
|
end
|
|
|
|
-- Update monitor from viewport rectangle (we'll use this info to clamp windows and save windows lost in a removed monitor)
|
|
--- @param viewport ImGuiViewportP
|
|
function ImGui.UpdateViewportPlatformMonitor(viewport)
|
|
viewport.PlatformMonitor = ImGui.FindPlatformMonitorForRect(viewport:GetMainRect())
|
|
end
|
|
|
|
-- Return value is always ~= nil, but don't hold on it across frames.
|
|
--- @param viewport_p ImGuiViewport
|
|
--- @return ImGuiPlatformMonitor
|
|
function ImGui.GetViewportPlatformMonitor(viewport_p)
|
|
local g = GImGui
|
|
local viewport = viewport_p --- @cast viewport ImGuiViewportP
|
|
|
|
local monitor_idx = viewport.PlatformMonitor
|
|
if monitor_idx >= 1 and monitor_idx <= g.PlatformIO.Monitors.Size then
|
|
return g.PlatformIO.Monitors.Data[monitor_idx]
|
|
end
|
|
return g.FallbackMonitor
|
|
end
|
|
|
|
--- @param viewport ImGuiViewportP
|
|
function ImGui.DestroyPlatformWindow(viewport)
|
|
local g = GImGui
|
|
if viewport.PlatformWindowCreated then
|
|
IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Destroy Platform Window %08X '%s'", viewport.ID, viewport.Window and viewport.Window.Name or "n/a")
|
|
if g.PlatformIO.Renderer_DestroyWindow then
|
|
g.PlatformIO.Renderer_DestroyWindow(viewport)
|
|
end
|
|
if g.PlatformIO.Platform_DestroyWindow then
|
|
g.PlatformIO.Platform_DestroyWindow(viewport)
|
|
end
|
|
IM_ASSERT(viewport.RendererUserData == nil and viewport.PlatformUserData == nil)
|
|
|
|
-- Don't clear PlatformWindowCreated for the main viewport, as we initially set that up to true in Initialize()
|
|
-- The righter way may be to leave it to the backend to set this flag all-together, and made the flag public.
|
|
if viewport.ID ~= IMGUI_VIEWPORT_DEFAULT_ID then
|
|
viewport.PlatformWindowCreated = false
|
|
end
|
|
else
|
|
IM_ASSERT(viewport.RendererUserData == nil and viewport.PlatformUserData == nil and viewport.PlatformHandle == nil)
|
|
end
|
|
viewport.RendererUserData = nil
|
|
viewport.PlatformUserData = nil
|
|
viewport.PlatformHandle = nil
|
|
viewport:ClearRequestFlags()
|
|
end
|
|
|
|
function ImGui.DestroyPlatformWindows()
|
|
-- We call the destroy window on every viewport (including the main viewport, index 1) to give a chance to the backend
|
|
-- to clear any data they may have stored in e.g. PlatformUserData, RendererUserData.
|
|
-- It is convenient for the platform backend code to store something in the main viewport, in order for e.g. the mouse handling
|
|
-- code to operator a consistent manner.
|
|
-- It is expected that the backend can handle calls to Renderer_DestroyWindow/Platform_DestroyWindow without
|
|
-- crashing if it doesn't have data stored.
|
|
local g = GImGui
|
|
for _, viewport in g.Viewports:iter() do
|
|
ImGui.DestroyPlatformWindow(viewport)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] METRICS/DEBUGGER WINDOW
|
|
-------------------------------------------------------------------------------
|
|
-- - DebugRenderViewportThumbnail() [Internal]
|
|
-- - RenderViewportsThumbnails() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @param draw_list ImDrawList
|
|
--- @param viewport ImGuiViewport
|
|
--- @param bb ImRect
|
|
function ImGui.DebugRenderViewportThumbnail(draw_list, viewport, bb)
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
local scale = bb:GetSize() / viewport.Size
|
|
local off = bb.Min - viewport.Pos * scale
|
|
local alpha_mul = (bit32.band(viewport.Flags, ImGuiViewportFlags.IsMinimized) ~= 0) and 0.30 or 1.00
|
|
window.DrawList:AddRectFilled(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.Border, alpha_mul * 0.40))
|
|
|
|
for _, thumb_window in g.Windows:iter() do
|
|
repeat
|
|
if not thumb_window.WasActive or bit32.band(thumb_window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
break
|
|
end
|
|
if thumb_window.Viewport ~= viewport then
|
|
break
|
|
end
|
|
|
|
local thumb_r = thumb_window:Rect()
|
|
local title_r = thumb_window:TitleBarRect()
|
|
thumb_r = ImRect(ImTrunc(off + thumb_r.Min * scale), ImTrunc(off + thumb_r.Max * scale))
|
|
title_r = ImRect(ImTrunc(off + title_r.Min * scale), ImTrunc(off + ImVec2(title_r.Max.x, title_r.Min.y + title_r:GetHeight() * 3.0) * scale)) -- Exaggerate title bar height
|
|
thumb_r:ClipWithFull(bb)
|
|
title_r:ClipWithFull(bb)
|
|
local window_is_focused = g.NavWindow and thumb_window.RootWindowForTitleBarHighlight == g.NavWindow.RootWindowForTitleBarHighlight
|
|
window.DrawList:AddRectFilled(thumb_r.Min, thumb_r.Max, ImGui.GetColorU32(ImGuiCol.WindowBg, alpha_mul))
|
|
window.DrawList:AddRectFilled(title_r.Min, title_r.Max, ImGui.GetColorU32(window_is_focused and ImGuiCol.TitleBgActive or ImGuiCol.TitleBg, alpha_mul))
|
|
window.DrawList:AddRect(thumb_r.Min, thumb_r.Max, ImGui.GetColorU32(ImGuiCol.Border, alpha_mul))
|
|
window.DrawList:AddText(g.Font, g.FontSize * 1.0, title_r.Min, ImGui.GetColorU32(ImGuiCol.Text, alpha_mul), thumb_window.Name, 1, ImGui.FindRenderedTextEnd(thumb_window.Name))
|
|
|
|
until true
|
|
end
|
|
|
|
draw_list:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.Border, alpha_mul))
|
|
if viewport.ID == g.DebugMetricsConfig.HighlightViewportID then
|
|
window.DrawList:AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255))
|
|
end
|
|
end
|
|
|
|
function ImGui.RenderViewportsThumbnails()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
|
|
-- Draw monitor and calculate their boundaries
|
|
local SCALE = 1.0 / 8.0
|
|
local bb_full = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX)
|
|
for _, monitor in g.PlatformIO.Monitors:iter() do
|
|
bb_full:Add(ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize))
|
|
end
|
|
|
|
local p = window.DC.CursorPos
|
|
local off = p - bb_full.Min * SCALE
|
|
for _, monitor in g.PlatformIO.Monitors:iter() do
|
|
local monitor_draw_bb = ImRect(off + monitor.MainPos * SCALE, off + (monitor.MainPos + monitor.MainSize) * SCALE)
|
|
window.DrawList:AddRect(monitor_draw_bb.Min, monitor_draw_bb.Max, (g.DebugMetricsConfig.HighlightMonitorIdx == g.PlatformIO.Monitors:index_from_ptr(monitor)) and IM_COL32(255, 255, 0, 255) or ImGui.GetColorU32(ImGuiCol.Border), 4.0)
|
|
window.DrawList:AddRectFilled(monitor_draw_bb.Min, monitor_draw_bb.Max, ImGui.GetColorU32(ImGuiCol.Border, 0.10), 4.0)
|
|
end
|
|
|
|
-- Draw viewports
|
|
for _, viewport in g.Viewports:iter() do
|
|
local viewport_draw_bb = ImRect(off + viewport.Pos * SCALE, off + (viewport.Pos + viewport.Size) * SCALE)
|
|
ImGui.DebugRenderViewportThumbnail(window.DrawList, viewport, viewport_draw_bb)
|
|
end
|
|
ImGui.Dummy(bb_full:GetSize() * SCALE)
|
|
end
|
|
|
|
|
|
|
|
|
|
--- @param idx ImGuiCol
|
|
--- @return ImVec4 # live reference to the style color (like upstream's const&)
|
|
function ImGui.GetStyleColorVec4(idx)
|
|
return GImGui.Style.Colors[idx]
|
|
end
|
|
|
|
-- Until 1.89 (August 2022, IMGUI_VERSION_NUM < 18814) it was legal to use SetCursorPos()/SetCursorScreenPos()
|
|
-- to extend contents size of our parent container (e.g. window contents size, which is used for auto-resizing
|
|
-- windows, table column contents size used for auto-resizing columns, group size).
|
|
-- This was causing issues and ambiguities and we needed to retire that.
|
|
--
|
|
-- Advanced:
|
|
-- - For reference, old logic was causing issues because it meant that SetCursorScreenPos(GetCursorScreenPos())
|
|
-- had a side-effect on layout! In particular this caused problem to compute group boundaries.
|
|
-- e.g. BeginGroup() + SomeItem() + SetCursorScreenPos(GetCursorScreenPos()) + EndGroup() would cause the
|
|
-- group to be taller because auto-sizing generally adds padding on bottom and right side.
|
|
-- - While this code is a little twisted, no-one would expect SetXXX(GetXXX()) to have a side-effect.
|
|
-- Using vertical alignment patterns would frequently trigger this sorts of issue.
|
|
-- - See https://github.com/ocornut/imgui/issues/5548 for more details.
|
|
function ImGui.ErrorCheckUsingSetCursorPosToExtendParentBoundaries()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT(window.DC.IsSetPos)
|
|
window.DC.IsSetPos = false
|
|
if window.DC.CursorPos.x <= window.DC.CursorMaxPos.x and window.DC.CursorPos.y <= window.DC.CursorMaxPos.y then return end
|
|
if window.SkipItems then return end
|
|
-- Upstream asserts (user error): "Code uses SetCursorPos()/SetCursorScreenPos() to extend window/parent boundaries.
|
|
-- Please submit an item e.g. Dummy() afterwards". We keep the old, tolerant behavior instead of erroring.
|
|
window.DC.CursorMaxPos = ImMax(window.DC.CursorMaxPos, window.DC.CursorPos)
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] POPUPS: modals, context popups, dimming
|
|
-------------------------------------------------------------------------------
|
|
|
|
local function GetWindowDisplayLayer(window)
|
|
return (bit32.band(window.Flags, ImGuiWindowFlags.Tooltip) ~= 0) and 1 or 0
|
|
end
|
|
|
|
--- @return int # 0-based display index (like upstream), -1 if not found
|
|
function ImGui.FindWindowDisplayIndex(window)
|
|
local g = GImGui
|
|
local i = g.Windows:find_index(window)
|
|
return i and (i - 1) or -1
|
|
end
|
|
|
|
function ImGui.BringWindowToDisplayBack(window)
|
|
local g = GImGui
|
|
if g.Windows.Data[1] == window then return end
|
|
local i = g.Windows:find_index(window)
|
|
if i then
|
|
table.remove(g.Windows.Data, i)
|
|
table.insert(g.Windows.Data, 1, window)
|
|
end
|
|
end
|
|
|
|
function ImGui.FindBottomMostVisibleWindowWithinBeginStack(parent_window)
|
|
local g = GImGui
|
|
local bottom_most_visible_window = parent_window
|
|
for i = ImGui.FindWindowDisplayIndex(parent_window), 0, -1 do
|
|
local window = g.Windows.Data[i + 1]
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0 then
|
|
if not ImGui.IsWindowWithinBeginStackOf(window, parent_window) then break end
|
|
if ImGui.IsWindowActiveAndVisible(window) and GetWindowDisplayLayer(window) <= GetWindowDisplayLayer(parent_window) then
|
|
bottom_most_visible_window = window
|
|
end
|
|
end
|
|
end
|
|
return bottom_most_visible_window
|
|
end
|
|
|
|
-- See Demo->Stacked Modal to confirm what this is for.
|
|
function ImGui.GetTopMostAndVisiblePopupModal()
|
|
local g = GImGui
|
|
for n = g.OpenPopupStack.Size, 1, -1 do
|
|
local popup = g.OpenPopupStack.Data[n].Window
|
|
if popup and bit32.band(popup.Flags, ImGuiWindowFlags.Modal) ~= 0 and ImGui.IsWindowActiveAndVisible(popup) then
|
|
return popup
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function ImGui.RenderDimmedBackgroundBehindWindow(window, col)
|
|
if bit32.band(col, IM_COL32_A_MASK) == 0 then return end
|
|
local viewport = window.Viewport
|
|
local viewport_rect = viewport:GetMainRect()
|
|
-- Draw behind window by moving the draw command at the FRONT of the draw list
|
|
-- Draw list have been trimmed already, hence the explicit recreation of a draw command if missing.
|
|
-- FIXME: This is a little bit complicated, solely to avoid creating/injecting an extra drawlist in drawdata.
|
|
local draw_list = (window.RootWindowDockTree or window.RootWindow).DrawList
|
|
draw_list:ChannelsMerge()
|
|
if draw_list.CmdBuffer.Size == 0 then draw_list:AddDrawCmd() end
|
|
draw_list:PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false) -- FIXME: Need to strictly ensure ImDrawCmd are not merged
|
|
draw_list:AddRectFilled(viewport_rect.Min, viewport_rect.Max, col)
|
|
local cmd = draw_list.CmdBuffer.Data[draw_list.CmdBuffer.Size]
|
|
draw_list.CmdBuffer:pop_back()
|
|
draw_list.CmdBuffer:push_front(cmd)
|
|
draw_list:AddDrawCmd() -- We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command.
|
|
draw_list:PopClipRect()
|
|
end
|
|
|
|
-- Important: AddWindowToDrawData() has not been called yet, meaning DockNodeHost windows needs a DrawList:ChannelsMerge() before usage.
|
|
-- We call ChannelsMerge() lazily here at it is faster that doing a full iteration of g.Windows[] prior to calling RenderDimmedBackgrounds().
|
|
function ImGui.RenderDimmedBackgrounds()
|
|
local g = GImGui
|
|
local modal_window = ImGui.GetTopMostAndVisiblePopupModal()
|
|
if g.DimBgRatio <= 0.0 and g.NavWindowingHighlightAlpha <= 0.0 then return end
|
|
local dim_bg_for_modal = (modal_window ~= nil)
|
|
local dim_bg_for_window_list = (g.NavWindowingTargetAnim ~= nil and g.NavWindowingTargetAnim.Active)
|
|
if not dim_bg_for_modal and not dim_bg_for_window_list then return end
|
|
|
|
if dim_bg_for_modal then
|
|
-- Draw dimming behind modal or a begin stack child, whichever comes first in draw order.
|
|
local dim_behind_window = ImGui.FindBottomMostVisibleWindowWithinBeginStack(modal_window)
|
|
ImGui.RenderDimmedBackgroundBehindWindow(dim_behind_window, ImGui.GetColorU32(modal_window.DC.ModalDimBgColor, g.DimBgRatio, true))
|
|
elseif dim_bg_for_window_list then
|
|
-- Draw dimming behind Ctrl+Tab target window and behind Ctrl+Tab UI window
|
|
ImGui.RenderDimmedBackgroundBehindWindow(g.NavWindowingTargetAnim, ImGui.GetColorU32(ImGuiCol.NavWindowingDimBg, g.DimBgRatio))
|
|
if g.NavWindowingListWindow ~= nil and g.NavWindowingListWindow.Active and g.NavWindowingListWindow.Viewport and g.NavWindowingListWindow.Viewport ~= g.NavWindowingTargetAnim.Viewport then
|
|
ImGui.RenderDimmedBackgroundBehindWindow(g.NavWindowingListWindow, ImGui.GetColorU32(ImGuiCol.NavWindowingDimBg, g.DimBgRatio))
|
|
end
|
|
|
|
-- Draw border around Ctrl+Tab target window
|
|
local window = g.NavWindowingTargetAnim
|
|
local viewport = window.Viewport
|
|
local distance = g.FontSize
|
|
local bb = window:Rect()
|
|
bb:Expand(distance)
|
|
if bb:GetWidth() >= viewport.Size.x and bb:GetHeight() >= viewport.Size.y then bb:Expand(-distance - 1.0) end -- If a window fits the entire viewport, adjust its highlight inward
|
|
window.DrawList:ChannelsMerge()
|
|
if window.DrawList.CmdBuffer.Size == 0 then window.DrawList:AddDrawCmd() end
|
|
window.DrawList:PushClipRect(viewport.Pos, viewport.Pos + viewport.Size)
|
|
window.DrawList:AddRect(bb.Min, bb.Max, ImGui.GetColorU32(ImGuiCol.NavWindowingHighlight, g.NavWindowingHighlightAlpha), window.WindowRounding, 0, 3.0) -- FIXME-DPI
|
|
window.DrawList:PopClipRect()
|
|
end
|
|
end
|
|
|
|
function ImGui.ClosePopupsExceptModals()
|
|
local g = GImGui
|
|
local popup_count_to_keep = g.OpenPopupStack.Size
|
|
while popup_count_to_keep > 0 do
|
|
local window = g.OpenPopupStack.Data[popup_count_to_keep].Window
|
|
if not window or bit32.band(window.Flags, ImGuiWindowFlags.Modal) ~= 0 then break end
|
|
popup_count_to_keep = popup_count_to_keep - 1
|
|
end
|
|
if popup_count_to_keep < g.OpenPopupStack.Size then -- This test is not required but it allows to set a convenient breakpoint on the statement below
|
|
ImGui.ClosePopupToLevel(popup_count_to_keep, true)
|
|
end
|
|
end
|
|
|
|
-- If 'p_open' is specified for a modal popup window, the popup will have a regular close button which will close the popup.
|
|
-- Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup).
|
|
-- - p_open is returned set back to false by BeginPopupModal() when popup is not open.
|
|
-- - if you set p_open to false before calling BeginPopupModal(), it will close the popup.
|
|
--- @return bool visible, bool? p_open # p_open is the updated value of the optional close button flag
|
|
function ImGui.BeginPopupModal(name, p_open, flags)
|
|
if flags == nil then flags = 0 end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local id = window:GetID(name)
|
|
if not ImGui.IsPopupOpen(id, ImGuiPopupFlags.None) then
|
|
g.NextWindowData:ClearFlags() -- We behave like Begin() and need to consume those values
|
|
if p_open then p_open = false end
|
|
return false, p_open
|
|
end
|
|
|
|
-- Center modal windows by default for increased visibility
|
|
-- (this won't really last as settings will kick in, and is mostly for backward compatibility. user may do the same themselves)
|
|
-- FIXME: Should test for (PosCond & window.SetWindowPosAllowFlags) with the upcoming window.
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasPos) == 0 then
|
|
local viewport = window.WasActive and window.Viewport or ImGui.GetMainViewport() -- FIXME-VIEWPORT: What may be our reference viewport?
|
|
ImGui.SetNextWindowPos(viewport:GetCenter(), ImGuiCond.FirstUseEver, ImVec2(0.5, 0.5))
|
|
end
|
|
|
|
flags = bit32.bor(flags, ImGuiWindowFlags.Popup, ImGuiWindowFlags.Modal, ImGuiWindowFlags.NoCollapse, ImGuiWindowFlags.NoDocking)
|
|
local is_open
|
|
p_open, is_open = ImGui.Begin(name, p_open, flags)
|
|
if not is_open or (p_open ~= nil and not p_open) then -- NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display)
|
|
ImGui.EndPopup()
|
|
if is_open then ImGui.ClosePopupToLevel(g.BeginPopupStack.Size, true) end
|
|
return false, p_open
|
|
end
|
|
return is_open, p_open
|
|
end
|
|
|
|
function ImGui.IsPopupOpenRequestForWindow(popup_flags)
|
|
local g = GImGui
|
|
local mouse_button = ImGui.GetMouseButtonFromPopupFlags(popup_flags)
|
|
if ImGui.IsMouseReleased(mouse_button, ImGuiKeyOwner_NoOwner) and ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByPopup) then
|
|
if bit32.band(popup_flags, ImGuiPopupFlags.NoOpenOverItems) == 0 or not ImGui.IsAnyItemHovered() then
|
|
return true
|
|
end
|
|
end
|
|
if g.NavOpenContextMenuWindowId and g.NavOpenContextMenuWindowId ~= 0 and g.CurrentWindow.ID ~= 0 then
|
|
if ImGui.IsWindowChildOf(g.NavWindow, g.CurrentWindow, false, false) then return true end -- This enable ordering to be used to disambiguate item vs window (#8803)
|
|
end
|
|
return false
|
|
end
|
|
|
|
function ImGui.BeginPopupContextWindow(str_id, popup_flags)
|
|
if popup_flags == nil then popup_flags = 0 end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if not str_id then str_id = "window_context" end
|
|
local id = window:GetID(str_id)
|
|
if ImGui.IsPopupOpenRequestForWindow(popup_flags) then ImGui.OpenPopupEx(id, popup_flags) end
|
|
return ImGui.BeginPopupEx(id, bit32.bor(ImGuiWindowFlags.AlwaysAutoResize, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoSavedSettings))
|
|
end
|
|
|
|
function ImGui.BeginPopupContextVoid(str_id, popup_flags)
|
|
if popup_flags == nil then popup_flags = 0 end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if not str_id then str_id = "void_context" end
|
|
local id = window:GetID(str_id) -- FIXME: Use a global ID?
|
|
local mouse_button = ImGui.GetMouseButtonFromPopupFlags(popup_flags)
|
|
if ImGui.IsMouseReleased(mouse_button, id) and not ImGui.IsWindowHovered(ImGuiHoveredFlags.AnyWindow) then
|
|
if ImGui.GetTopMostPopupModal() == nil then ImGui.OpenPopupEx(id, popup_flags) end
|
|
end
|
|
return ImGui.BeginPopupEx(id, bit32.bor(ImGuiWindowFlags.AlwaysAutoResize, ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoSavedSettings))
|
|
end
|
|
|
|
function ImGui.BeginTooltipHidden()
|
|
local g = GImGui
|
|
local _, ret = ImGui.Begin("##Tooltip_Hidden", nil, bit32.bor(ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.NoInputs, ImGuiWindowFlags.NoTitleBar,
|
|
ImGuiWindowFlags.NoMove, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoSavedSettings, ImGuiWindowFlags.AlwaysAutoResize))
|
|
ImGui.SetWindowHiddenAndSkipItemsForCurrentFrame(g.CurrentWindow)
|
|
return ret
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] MISC GETTERS/SETTERS, FOCUS
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGui.NavMoveRequestResolveWithLastItem(result)
|
|
local g = GImGui
|
|
g.NavMoveScoringItems = false -- Ensure request doesn't need more processing
|
|
ImGui.NavApplyItemToResult(result)
|
|
ImGui.NavUpdateAnyRequestFlag()
|
|
end
|
|
|
|
-- Note: this will likely be called ActivateItem() once we rework our Focus/Activation system!
|
|
-- But ActivateItem() should function without altering scroll/focus?
|
|
function ImGui.SetKeyboardFocusHere(offset)
|
|
if offset == nil then offset = 0 end
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
IM_ASSERT(offset >= -1) -- -1 is allowed but not below
|
|
|
|
-- It makes sense in the vast majority of cases to never interrupt a drag and drop.
|
|
-- When we refactor this function into ActivateItem() we may want to make this an option.
|
|
-- MovingWindow is protected from most user inputs using SetActiveIdUsingNavAndKeys(), but
|
|
-- is also automatically dropped in the event g.ActiveId is stolen.
|
|
if g.DragDropActive or g.MovingWindow ~= nil then
|
|
return
|
|
end
|
|
|
|
ImGui.SetNavWindow(window)
|
|
|
|
local move_flags = bit32.bor(ImGuiNavMoveFlags.IsTabbing, ImGuiNavMoveFlags.Activate, ImGuiNavMoveFlags.FocusApi, ImGuiNavMoveFlags.NoSetNavCursorVisible)
|
|
local scroll_flags = window.Appearing and bit32.bor(ImGuiScrollFlags.KeepVisibleEdgeX, ImGuiScrollFlags.AlwaysCenterY) or bit32.bor(ImGuiScrollFlags.KeepVisibleEdgeX, ImGuiScrollFlags.KeepVisibleEdgeY)
|
|
ImGui.NavMoveRequestSubmit(ImGuiDir.None, (offset < 0) and ImGuiDir.Up or ImGuiDir.Down, move_flags, scroll_flags) -- FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable.
|
|
if offset == -1 then
|
|
ImGui.NavMoveRequestResolveWithLastItem(g.NavMoveResultLocal)
|
|
else
|
|
g.NavTabbingDir = 1
|
|
g.NavTabbingCounter = offset + 1
|
|
end
|
|
end
|
|
|
|
-- Focus = move navigation cursor, set scrolling, set focus window.
|
|
function ImGui.FocusItem()
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
if g.DragDropActive or g.MovingWindow ~= nil then -- FIXME: Opt-in flags for this?
|
|
return
|
|
end
|
|
local move_flags = bit32.bor(ImGuiNavMoveFlags.IsTabbing, ImGuiNavMoveFlags.FocusApi, ImGuiNavMoveFlags.NoSetNavCursorVisible, ImGuiNavMoveFlags.NoSelect)
|
|
local scroll_flags = window.Appearing and bit32.bor(ImGuiScrollFlags.KeepVisibleEdgeX, ImGuiScrollFlags.AlwaysCenterY) or bit32.bor(ImGuiScrollFlags.KeepVisibleEdgeX, ImGuiScrollFlags.KeepVisibleEdgeY)
|
|
ImGui.SetNavWindow(window)
|
|
ImGui.NavMoveRequestSubmit(ImGuiDir.None, ImGuiDir.Up, move_flags, scroll_flags)
|
|
ImGui.NavMoveRequestResolveWithLastItem(g.NavMoveResultLocal)
|
|
end
|
|
|
|
function ImGui.ActivateItemByID(id)
|
|
local g = GImGui
|
|
g.NavNextActivateId = id
|
|
g.NavNextActivateFlags = ImGuiActivateFlags.None
|
|
end
|
|
|
|
function ImGui.SetNavCursorVisible(visible)
|
|
local g = GImGui
|
|
if g.NavWindow and bit32.band(g.NavWindow.Flags, ImGuiWindowFlags.NoNavInputs) ~= 0 then
|
|
visible = false
|
|
elseif g.IO.ConfigNavCursorVisibleAlways then
|
|
visible = true
|
|
end
|
|
g.NavCursorVisible = visible
|
|
end
|
|
|
|
function ImGui.GetFrameCount() return GImGui.FrameCount end
|
|
function ImGui.GetStateStorage() return GImGui.CurrentWindow.DC.StateStorage end
|
|
function ImGui.SetStateStorage(tree)
|
|
local window = GImGui.CurrentWindow
|
|
window.DC.StateStorage = tree or window.StateStorage
|
|
end
|
|
function ImGui.SetNextFrameWantCaptureKeyboard(want) GImGui.WantCaptureKeyboardNextFrame = want and 1 or 0 end
|
|
function ImGui.SetNextFrameWantCaptureMouse(want) GImGui.WantCaptureMouseNextFrame = want and 1 or 0 end
|
|
function ImGui.GetVersion() return IMGUI_VERSION end
|
|
|
|
local StyleColorNames = nil
|
|
function ImGui.GetStyleColorName(idx)
|
|
if StyleColorNames == nil then
|
|
StyleColorNames = {}
|
|
for k, v in pairs(ImGuiCol) do if k ~= "COUNT" then StyleColorNames[v] = k end end
|
|
end
|
|
return StyleColorNames[idx] or "Unknown"
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] NAV END FRAME
|
|
-------------------------------------------------------------------------------
|
|
|
|
local NAV_WINDOWING_LIST_APPEAR_DELAY = 0.15
|
|
|
|
-- Window has already passed the IsWindowNavFocusable()
|
|
local function GetFallbackWindowNameForWindowingList(window)
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.Popup) ~= 0 then
|
|
return "(Popup)"
|
|
end
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.MenuBar) ~= 0 and window.Name == "##MainMenuBar" then
|
|
return "(Main menu bar)"
|
|
end
|
|
if window.DockNodeAsHost then
|
|
return "(Dock node)" -- Not normally shown to user.
|
|
end
|
|
return "(Untitled)"
|
|
end
|
|
|
|
-- Overlay displayed when using Ctrl+Tab. Called by EndFrame().
|
|
function ImGui.NavUpdateWindowingOverlay()
|
|
local g = GImGui
|
|
IM_ASSERT(g.NavWindowingTarget ~= nil)
|
|
|
|
if g.NavWindowingTimer < NAV_WINDOWING_LIST_APPEAR_DELAY then
|
|
return
|
|
end
|
|
|
|
local viewport = ImGui.GetMainViewport()
|
|
ImGui.SetNextWindowSizeConstraints(ImVec2(viewport.Size.x * 0.20, viewport.Size.y * 0.20), ImVec2(FLT_MAX, FLT_MAX))
|
|
ImGui.SetNextWindowPos(viewport:GetCenter(), ImGuiCond.Always, ImVec2(0.5, 0.5))
|
|
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, g.Style.WindowPadding * 2.0)
|
|
ImGui.Begin("##NavWindowingOverlay", nil, bit32.bor(ImGuiWindowFlags.NoTitleBar, ImGuiWindowFlags.NoFocusOnAppearing, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoMove, ImGuiWindowFlags.NoInputs, ImGuiWindowFlags.AlwaysAutoResize, ImGuiWindowFlags.NoSavedSettings))
|
|
g.NavWindowingListWindow = g.CurrentWindow
|
|
for n = g.WindowsFocusOrder.Size, 1, -1 do
|
|
local window = g.WindowsFocusOrder.Data[n]
|
|
IM_ASSERT(window ~= nil) -- Fix static analyzers
|
|
if ImGui.IsWindowNavFocusable(window) then
|
|
local label = window.Name
|
|
if ImGui.FindRenderedTextEnd(label) == 1 then
|
|
label = GetFallbackWindowNameForWindowingList(window)
|
|
end
|
|
ImGui.Selectable(label, g.NavWindowingTarget == window)
|
|
end
|
|
end
|
|
ImGui.End()
|
|
ImGui.PopStyleVar()
|
|
end
|
|
|
|
local function NavUpdateCreateWrappingRequest()
|
|
local g = GImGui
|
|
local window = g.NavWindow
|
|
|
|
local do_forward = false
|
|
local src = window.NavRectRel[g.NavLayer]
|
|
local bb_rel = ImRect(src.Min.x, src.Min.y, src.Max.x, src.Max.y)
|
|
local clip_dir = g.NavMoveDir
|
|
|
|
local move_flags = g.NavMoveFlags
|
|
-- local move_axis = (g.NavMoveDir == ImGuiDir.Up or g.NavMoveDir == ImGuiDir.Down) and ImGuiAxis.Y or ImGuiAxis.X
|
|
|
|
-- Menu layer does not maintain scrolling / content size (#9178)
|
|
local wrap_size = (g.NavLayer == ImGuiNavLayer.Menu) and window.Size or (window.ContentSize + window.WindowPadding)
|
|
|
|
if g.NavMoveDir == ImGuiDir.Left and bit32.band(move_flags, bit32.bor(ImGuiNavMoveFlags.WrapX, ImGuiNavMoveFlags.LoopX)) ~= 0 then
|
|
bb_rel.Min.x = wrap_size.x; bb_rel.Max.x = wrap_size.x
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.WrapX) ~= 0 then
|
|
bb_rel:TranslateY(-bb_rel:GetHeight()) -- Previous row
|
|
clip_dir = ImGuiDir.Up
|
|
end
|
|
do_forward = true
|
|
end
|
|
if g.NavMoveDir == ImGuiDir.Right and bit32.band(move_flags, bit32.bor(ImGuiNavMoveFlags.WrapX, ImGuiNavMoveFlags.LoopX)) ~= 0 then
|
|
bb_rel.Min.x = -window.WindowPadding.x; bb_rel.Max.x = -window.WindowPadding.x
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.WrapX) ~= 0 then
|
|
bb_rel:TranslateY(bb_rel:GetHeight()) -- Next row
|
|
clip_dir = ImGuiDir.Down
|
|
end
|
|
do_forward = true
|
|
end
|
|
if g.NavMoveDir == ImGuiDir.Up and bit32.band(move_flags, bit32.bor(ImGuiNavMoveFlags.WrapY, ImGuiNavMoveFlags.LoopY)) ~= 0 then
|
|
bb_rel.Min.y = wrap_size.y; bb_rel.Max.y = wrap_size.y
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.WrapY) ~= 0 then
|
|
bb_rel:TranslateX(-bb_rel:GetWidth()) -- Previous column
|
|
clip_dir = ImGuiDir.Left
|
|
end
|
|
do_forward = true
|
|
end
|
|
if g.NavMoveDir == ImGuiDir.Down and bit32.band(move_flags, bit32.bor(ImGuiNavMoveFlags.WrapY, ImGuiNavMoveFlags.LoopY)) ~= 0 then
|
|
bb_rel.Min.y = -window.WindowPadding.y; bb_rel.Max.y = -window.WindowPadding.y
|
|
if bit32.band(move_flags, ImGuiNavMoveFlags.WrapY) ~= 0 then
|
|
bb_rel:TranslateX(bb_rel:GetWidth()) -- Next column
|
|
clip_dir = ImGuiDir.Right
|
|
end
|
|
do_forward = true
|
|
end
|
|
if not do_forward then
|
|
return
|
|
end
|
|
window.NavRectRel[g.NavLayer] = bb_rel
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.X)
|
|
ImGui.NavClearPreferredPosForAxis(ImGuiAxis.Y)
|
|
ImGui.NavMoveRequestForward(g.NavMoveDir, clip_dir, move_flags, g.NavMoveScrollFlags)
|
|
end
|
|
|
|
function ImGui.NavEndFrame()
|
|
local g = GImGui
|
|
|
|
-- Show Ctrl+Tab list window
|
|
if g.NavWindowingTarget ~= nil then
|
|
ImGui.NavUpdateWindowingOverlay()
|
|
end
|
|
|
|
-- Perform wrap-around in menus
|
|
-- FIXME-NAV: Wrap may need to apply a weight bias on the other axis. e.g. 4x4 grid with 2 last items missing on last item won't handle LoopY/WrapY correctly.
|
|
-- FIXME-NAV: Wrap (not Loop) support could be handled by the scoring function and then WrapX would function without an extra frame.
|
|
if g.NavWindow and ImGui.NavMoveRequestButNoResultYet() and bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.WrapMask_) ~= 0 and bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.Forwarded) == 0 then
|
|
NavUpdateCreateWrappingRequest()
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] ImGuiTextFilter
|
|
-------------------------------------------------------------------------------
|
|
|
|
--- @class ImGuiTextFilter
|
|
MT.ImGuiTextFilter = {}
|
|
MT.ImGuiTextFilter.__index = MT.ImGuiTextFilter
|
|
|
|
-- Helper: Parse and apply text filters e.g. 'aaa bbb -ccc'.
|
|
--- @param default_filter? string
|
|
function ImGuiTextFilter(default_filter)
|
|
local this = setmetatable({ InputBuf = {}, _CountExclude = 0, _Items = ImVector() }, MT.ImGuiTextFilter)
|
|
for i = 1, 256 do this.InputBuf[i] = 0 end
|
|
if default_filter and default_filter ~= "" then
|
|
for i = 1, ImMin(#default_filter, 255) do this.InputBuf[i] = string.byte(default_filter, i) end
|
|
this:Build()
|
|
end
|
|
return this
|
|
end
|
|
|
|
function MT.ImGuiTextFilter:Draw(label, width)
|
|
if label == nil then label = "Filter (inc,-exc)" end
|
|
if width ~= nil and width ~= 0.0 then ImGui.SetNextItemWidth(width) end
|
|
return self:DrawWithHint(label, "incl -excl")
|
|
end
|
|
|
|
-- Use ImGui.SetNextItemWidth() manually if you want to use this.
|
|
function MT.ImGuiTextFilter:DrawWithHint(label, hint)
|
|
if label == nil then label = "Filter" end
|
|
if hint == nil then hint = "incl -excl" end
|
|
local value_changed = ImGui.InputTextWithHint(label, hint, self.InputBuf, 256)
|
|
if value_changed then
|
|
self:Build()
|
|
end
|
|
return value_changed
|
|
end
|
|
|
|
function MT.ImGuiTextFilter:Clear() self.InputBuf[1] = 0; self:Build() end
|
|
function MT.ImGuiTextFilter:IsActive() return self._Items.Size ~= 0 end
|
|
|
|
-- Parse filter and split into a format optimal for PassFilter()
|
|
-- Items are { Text = string (lowercased), CountInclude = int }
|
|
function MT.ImGuiTextFilter:Build()
|
|
local items = self._Items
|
|
items:resize(0)
|
|
self._CountExclude = 0
|
|
|
|
local n = 0
|
|
while self.InputBuf[n + 1] ~= nil and self.InputBuf[n + 1] ~= 0 do n = n + 1 end
|
|
local buf = ImGui._ByteArrayToString(self.InputBuf, 1, n + 1)
|
|
local buf_e = n + 1 -- exclusive, 1-based
|
|
local word_b = 1
|
|
local seq_start_idx = -1 -- 1-based index into items, -1 = none
|
|
while word_b < buf_e do
|
|
-- Trim blanks
|
|
while word_b < buf_e and ImCharIsBlankA(string.byte(buf, word_b)) do
|
|
word_b = word_b + 1
|
|
end
|
|
local is_excl = word_b < buf_e and string.byte(buf, word_b) == 45 -- '-'
|
|
if is_excl then word_b = word_b + 1 end
|
|
local is_quote = word_b < buf_e and string.byte(buf, word_b) == 34 -- '"'
|
|
local word_e
|
|
if is_quote then
|
|
-- Parsing quotes. Omit storing leading/trailing quotes.
|
|
word_b = word_b + 1
|
|
word_e = string.find(buf, '"', word_b, true) or buf_e
|
|
if word_e > buf_e then word_e = buf_e end
|
|
else
|
|
-- Handle both ' ' and ',' separators.
|
|
word_e = word_b
|
|
while word_e < buf_e do
|
|
local c = string.byte(buf, word_e)
|
|
if c == 32 or c == 44 then break end
|
|
word_e = word_e + 1
|
|
end
|
|
end
|
|
|
|
-- Min length
|
|
if word_e - word_b > 0 then
|
|
-- Add to list. The '-' is not stored in items but implicitly inferred using (n < CountExclude).
|
|
local item = { Text = string.lower(string.sub(buf, word_b, word_e - 1)), CountInclude = 0 }
|
|
if is_excl then
|
|
items:insert(self._CountExclude + 1, item)
|
|
self._CountExclude = self._CountExclude + 1
|
|
if seq_start_idx ~= -1 then seq_start_idx = seq_start_idx + 1 end
|
|
else
|
|
if seq_start_idx == -1 then seq_start_idx = items.Size + 1 end
|
|
items:push_back(item)
|
|
items.Data[seq_start_idx].CountInclude = items.Data[seq_start_idx].CountInclude + 1
|
|
end
|
|
end
|
|
|
|
-- Next sequence
|
|
if word_e < buf_e and string.byte(buf, word_e) == 44 then -- ','
|
|
seq_start_idx = -1
|
|
end
|
|
word_b = word_e + 1
|
|
end
|
|
end
|
|
|
|
-- FIXME: Could use a specialized ImStristr() + pre-convert our filter to uppercase.
|
|
--- @param text string?
|
|
--- @param text_end? int # exclusive, 1-based
|
|
function MT.ImGuiTextFilter:PassFilter(text, text_end)
|
|
local items = self._Items
|
|
if items.Size == 0 then return true end
|
|
if text == nil then text = "" end
|
|
if text_end ~= nil then text = string.sub(text, 1, text_end - 1) end
|
|
text = string.lower(text)
|
|
|
|
-- Filters are sorted so that '-' ones are always leading.
|
|
local seq = 1
|
|
local seq_excl_end = self._CountExclude + 1
|
|
while seq < seq_excl_end do
|
|
if string.find(text, items.Data[seq].Text, 1, true) then return false end
|
|
seq = seq + 1
|
|
end
|
|
|
|
-- Process includes
|
|
local seq_end = items.Size + 1
|
|
if seq == seq_end then return true end -- When no inclusion are specified (only exclusions) we implicitly pass
|
|
while seq < seq_end do
|
|
local seq_incl_end = seq + items.Data[seq].CountInclude
|
|
while seq < seq_incl_end do
|
|
if not string.find(text, items.Data[seq].Text, 1, true) then break end
|
|
seq = seq + 1
|
|
end
|
|
if seq == seq_incl_end then return true end -- All matched
|
|
seq = seq_incl_end -- Try next
|
|
end
|
|
return false
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] ImGuiListClipper
|
|
-------------------------------------------------------------------------------
|
|
|
|
ImGuiListClipperFlags = ImGuiListClipperFlags or { None = 0, NoSetTableRowCounters = bit32.lshift(1, 0) }
|
|
|
|
-- FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell.
|
|
-- The problem we have is that without a Begin/End scheme for rows using the clipper is ambiguous.
|
|
local function GetSkipItemForListClipping()
|
|
local g = GImGui
|
|
if g.CurrentTable then return g.CurrentTable.HostSkipItems end
|
|
return g.CurrentWindow.SkipItems
|
|
end
|
|
|
|
local function ClipperRangeFromIndices(min, max) return { Min = min, Max = max, PosToIndexConvert = false, PosToIndexOffsetMin = 0, PosToIndexOffsetMax = 0 } end
|
|
local function ClipperRangeFromPositions(y1, y2, off_min, off_max) return { Min = y1, Max = y2, PosToIndexConvert = true, PosToIndexOffsetMin = off_min, PosToIndexOffsetMax = off_max } end
|
|
|
|
-- `offset` is a 0-based count of leading ranges to leave alone
|
|
local function ImGuiListClipper_SortAndFuseRanges(ranges, offset)
|
|
if offset == nil then offset = 0 end
|
|
if ranges.Size - offset <= 1 then return end
|
|
local d = ranges.Data
|
|
-- Helper to order ranges and fuse them together if possible (bubble sort is fine as we are only sorting 2-3 entries)
|
|
for sort_end = ranges.Size - offset - 1, 1, -1 do
|
|
for i = offset + 1, sort_end + offset do
|
|
if d[i].Min > d[i + 1].Min then d[i], d[i + 1] = d[i + 1], d[i] end
|
|
end
|
|
end
|
|
-- Now fuse ranges together as much as possible.
|
|
local i = 2 + offset
|
|
while i <= ranges.Size do
|
|
IM_ASSERT(not d[i].PosToIndexConvert and not d[i - 1].PosToIndexConvert)
|
|
if d[i - 1].Max < d[i].Min then
|
|
i = i + 1
|
|
else
|
|
d[i - 1].Min = ImMin(d[i - 1].Min, d[i].Min)
|
|
d[i - 1].Max = ImMax(d[i - 1].Max, d[i].Max)
|
|
ranges:erase(i)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ImGuiListClipper_SeekCursorAndSetupPrevLine(clipper, pos_y, line_height)
|
|
-- Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor.
|
|
-- FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue.
|
|
-- The clipper should probably have a final step to display the last item in a regular manner, maybe with an opt-out flag for data sets which may have costly seek?
|
|
local g = GImGui
|
|
local window = g.CurrentWindow
|
|
local off_y = pos_y - window.DC.CursorPos.y
|
|
window.DC.CursorPos.y = pos_y
|
|
window.DC.CursorMaxPos.y = ImMax(window.DC.CursorMaxPos.y, pos_y - g.Style.ItemSpacing.y)
|
|
window.DC.CursorPosPrevLine.y = window.DC.CursorPos.y - line_height -- Setting those fields so that SetScrollHereY() can properly function after the end of our clipper usage.
|
|
window.DC.PrevLineSize.y = (line_height - g.Style.ItemSpacing.y) -- If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list.
|
|
local columns = window.DC.CurrentColumns
|
|
if columns then
|
|
columns.LineMinY = window.DC.CursorPos.y -- Setting this so that cell Y position are set properly
|
|
end
|
|
local tbl = g.CurrentTable
|
|
if tbl then
|
|
if tbl.IsInsideRow then
|
|
ImGui.TableEndRow(tbl)
|
|
end
|
|
local row_increase = math.floor((off_y / line_height) + 0.5)
|
|
if row_increase > 0 and bit32.band(clipper.Flags, ImGuiListClipperFlags.NoSetTableRowCounters) == 0 then -- If your clipper item height is ~= from actual table row height, consider using ImGuiListClipperFlags.NoSetTableRowCounters. See #8886.
|
|
tbl.CurrentRow = tbl.CurrentRow + row_increase
|
|
tbl.RowBgColorCounter = tbl.RowBgColorCounter + row_increase
|
|
end
|
|
tbl.RowPosY2 = window.DC.CursorPos.y
|
|
end
|
|
end
|
|
|
|
--- @class ImGuiListClipper
|
|
MT.ImGuiListClipper = {}
|
|
MT.ImGuiListClipper.__index = MT.ImGuiListClipper
|
|
|
|
function ImGuiListClipper()
|
|
return setmetatable({ DisplayStart = 0, DisplayEnd = 0, UserIndex = 0, ItemsCount = 0, ItemsHeight = 0.0, Flags = 0,
|
|
StartPosY = 0.0, StartSeekOffsetY = 0.0, Ctx = nil, TempData = nil }, MT.ImGuiListClipper)
|
|
end
|
|
|
|
--- @param items_count int
|
|
--- @param items_height? float
|
|
function MT.ImGuiListClipper:Begin(items_count, items_height)
|
|
if items_height == nil then items_height = -1.0 end
|
|
self.Ctx = ImGui.GetCurrentContext()
|
|
local g = self.Ctx
|
|
local window = g.CurrentWindow
|
|
|
|
local tbl = g.CurrentTable
|
|
if tbl and tbl.IsInsideRow then
|
|
ImGui.TableEndRow(tbl)
|
|
end
|
|
|
|
self.StartPosY = window.DC.CursorPos.y
|
|
self.ItemsHeight = items_height
|
|
self.ItemsCount = items_count
|
|
self.DisplayStart = -1
|
|
self.DisplayEnd = 0
|
|
|
|
-- Acquire temporary buffer
|
|
g.ClipperTempDataStacked = g.ClipperTempDataStacked + 1
|
|
while g.ClipperTempDataStacked > g.ClipperTempData.Size do
|
|
g.ClipperTempData:push_back({ ListClipper = nil, LossynessOffset = 0.0, StepNo = 0, ItemsFrozen = 0, Ranges = ImVector() })
|
|
end
|
|
local data = g.ClipperTempData.Data[g.ClipperTempDataStacked]
|
|
data.ListClipper = self; data.StepNo = 0; data.ItemsFrozen = 0; data.Ranges:resize(0)
|
|
data.LossynessOffset = window.DC.CursorStartPosLossyness.y
|
|
self.TempData = data
|
|
self.StartSeekOffsetY = data.LossynessOffset
|
|
end
|
|
|
|
function MT.ImGuiListClipper:End()
|
|
local data = self.TempData
|
|
if data then
|
|
-- In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user.
|
|
local g = self.Ctx
|
|
if self.ItemsCount >= 0 and self.ItemsCount < INT_MAX and self.DisplayStart >= 0 then
|
|
self:SeekCursorForItem(self.ItemsCount)
|
|
end
|
|
|
|
-- Restore temporary buffer and fix back pointers which may be invalidated when nesting
|
|
IM_ASSERT(data.ListClipper == self)
|
|
data.StepNo = data.Ranges.Size
|
|
g.ClipperTempDataStacked = g.ClipperTempDataStacked - 1
|
|
if g.ClipperTempDataStacked > 0 then
|
|
data = g.ClipperTempData.Data[g.ClipperTempDataStacked]
|
|
data.ListClipper.TempData = data
|
|
end
|
|
self.TempData = nil
|
|
end
|
|
self.DisplayStart = self.ItemsCount -- Clear this so code which may be reused past last Step() won't trip on a non-empty range.
|
|
self.DisplayEnd = self.ItemsCount
|
|
self.ItemsCount = -1
|
|
end
|
|
|
|
function MT.ImGuiListClipper:IncludeItemByIndex(item_index) self:IncludeItemsByIndex(item_index, item_index + 1) end
|
|
|
|
function MT.ImGuiListClipper:IncludeItemsByIndex(item_begin, item_end)
|
|
local data = self.TempData
|
|
IM_ASSERT(self.DisplayStart < 0) -- Only allowed after Begin() and if there has not been a specified range yet.
|
|
IM_ASSERT(item_begin <= item_end)
|
|
if item_begin < item_end then
|
|
data.Ranges:push_back(ClipperRangeFromIndices(item_begin, item_end))
|
|
end
|
|
end
|
|
|
|
-- This is already called while stepping.
|
|
-- The ONLY reason you may want to call this is if you passed INT_MAX to ImGuiListClipper:Begin() because you couldn't step item count beforehand.
|
|
function MT.ImGuiListClipper:SeekCursorForItem(item_n)
|
|
-- - StartPosY starts from ItemsFrozen, by adding SeekOffsetY we generally cancel that out (SeekOffsetY == LossynessOffset - ItemsFrozen * ItemsHeight).
|
|
-- - The reason we store SeekOffsetY instead of inferring it, is because we want to allow user to perform Seek after the last step, where ImGuiListClipperData is already done.
|
|
local pos_y = self.StartPosY + self.StartSeekOffsetY + item_n * self.ItemsHeight
|
|
ImGuiListClipper_SeekCursorAndSetupPrevLine(self, pos_y, self.ItemsHeight)
|
|
end
|
|
|
|
local function ImGuiListClipper_StepInternal(clipper)
|
|
local g = clipper.Ctx
|
|
local window = g.CurrentWindow
|
|
local data = clipper.TempData
|
|
IM_ASSERT(data ~= nil, "Called ImGuiListClipper::Step() too many times, or before ImGuiListClipper::Begin() ?")
|
|
|
|
local tbl = g.CurrentTable
|
|
if tbl and tbl.IsInsideRow then
|
|
ImGui.TableEndRow(tbl)
|
|
end
|
|
|
|
-- No items
|
|
if clipper.ItemsCount == 0 or GetSkipItemForListClipping() then
|
|
return false
|
|
end
|
|
|
|
-- While we are in frozen row state, keep displaying items one by one, unclipped
|
|
-- FIXME: Could be stored as a table-agnostic state.
|
|
if data.StepNo == 0 and tbl ~= nil and not tbl.IsUnfrozenRows then
|
|
clipper.DisplayStart = data.ItemsFrozen
|
|
clipper.DisplayEnd = ImMin(data.ItemsFrozen + 1, clipper.ItemsCount)
|
|
if clipper.DisplayStart < clipper.DisplayEnd then
|
|
data.ItemsFrozen = data.ItemsFrozen + 1
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height)
|
|
local calc_clipping = false
|
|
if data.StepNo == 0 then
|
|
clipper.StartPosY = window.DC.CursorPos.y
|
|
if clipper.ItemsHeight <= 0.0 then
|
|
-- Submit the first item (or range) so we can measure its height (generally the first range is 0..1)
|
|
data.Ranges:push_front(ClipperRangeFromIndices(data.ItemsFrozen, data.ItemsFrozen + 1))
|
|
clipper.DisplayStart = ImMax(data.Ranges.Data[1].Min, data.ItemsFrozen)
|
|
clipper.DisplayEnd = ImMin(data.Ranges.Data[1].Max, clipper.ItemsCount)
|
|
data.StepNo = 1
|
|
return true
|
|
end
|
|
calc_clipping = true -- If on the first step with known item height, calculate clipping.
|
|
end
|
|
|
|
-- Step 1: Let the clipper infer height from first range
|
|
if clipper.ItemsHeight <= 0.0 then
|
|
IM_ASSERT(data.StepNo == 1)
|
|
-- (no float precision mitigation needed: Luau numbers are doubles)
|
|
clipper.ItemsHeight = (window.DC.CursorPos.y - clipper.StartPosY) / (clipper.DisplayEnd - clipper.DisplayStart)
|
|
if clipper.ItemsHeight == 0.0 and clipper.ItemsCount == INT_MAX then -- Accept that no item have been submitted if in indeterminate mode.
|
|
return false
|
|
end
|
|
if clipper.ItemsHeight <= 0.0 then
|
|
IM_ASSERT_USER_ERROR(clipper.ItemsHeight > 0.0, "ImGuiListClipper: Failed to calculate item height! First item hasn't been submitted by user code, or has not moved the cursor vertically!")
|
|
return false
|
|
end
|
|
calc_clipping = true -- If item height had to be calculated, calculate clipping afterwards.
|
|
end
|
|
|
|
-- Step 0 or 1: Calculate the actual ranges of visible elements.
|
|
local already_submitted = clipper.DisplayEnd
|
|
if calc_clipping then
|
|
-- Record seek offset, this is so ImGuiListClipper:Seek() can be called after ImGuiListClipperData is done
|
|
clipper.StartSeekOffsetY = data.LossynessOffset - data.ItemsFrozen * clipper.ItemsHeight
|
|
|
|
if g.LogEnabled then
|
|
-- If logging is active, do not perform any clipping
|
|
data.Ranges:push_back(ClipperRangeFromIndices(0, clipper.ItemsCount))
|
|
else
|
|
-- Add range selected to be included for navigation
|
|
local is_nav_request = (g.NavMoveScoringItems and g.NavWindow and g.NavWindow.RootWindowForNav == window.RootWindowForNav) and true or false
|
|
local nav_off_min = (is_nav_request and g.NavMoveClipDir == ImGuiDir.Up) and -1 or 0
|
|
local nav_off_max = (is_nav_request and g.NavMoveClipDir == ImGuiDir.Down) and 1 or 0
|
|
if is_nav_request then
|
|
data.Ranges:push_back(ClipperRangeFromPositions(g.NavScoringRect.Min.y, g.NavScoringRect.Max.y, nav_off_min, nav_off_max))
|
|
if not g.NavScoringNoClipRect:IsInverted() then
|
|
data.Ranges:push_back(ClipperRangeFromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, nav_off_min, nav_off_max))
|
|
end
|
|
end
|
|
if is_nav_request and bit32.band(g.NavMoveFlags, ImGuiNavMoveFlags.IsTabbing) ~= 0 and g.NavTabbingDir == -1 then
|
|
data.Ranges:push_back(ClipperRangeFromIndices(clipper.ItemsCount - 1, clipper.ItemsCount))
|
|
end
|
|
|
|
-- Add focused/active item
|
|
local nav_rect_abs = ImGui.WindowRectRelToAbs(window, window.NavRectRel[0])
|
|
if g.NavId ~= 0 and window.NavLastIds[0] == g.NavId then
|
|
data.Ranges:push_back(ClipperRangeFromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0))
|
|
end
|
|
|
|
local min_y = window.ClipRect.Min.y
|
|
local max_y = window.ClipRect.Max.y
|
|
|
|
-- Add box selection range
|
|
local bs = g.BoxSelectState
|
|
if bs and bs.IsActive and bs.Window == window then
|
|
-- FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos.
|
|
-- RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that.
|
|
-- As a workaround we currently half ItemSpacing worth on each side.
|
|
local pad_y = g.Style.ItemSpacing.y
|
|
min_y = min_y - pad_y
|
|
max_y = max_y + pad_y
|
|
|
|
-- Box-select on 2D area requires different clipping.
|
|
-- (best adding pad_y here than in BeginBoxSelect() as we are closer to current state)
|
|
if bs.UnclipMode then
|
|
data.Ranges:push_back(ClipperRangeFromPositions(bs.UnclipRect.Min.y - pad_y, bs.UnclipRect.Max.y + pad_y, 0, 0))
|
|
end
|
|
end
|
|
|
|
-- Add main visible range
|
|
data.Ranges:push_back(ClipperRangeFromPositions(min_y, max_y, nav_off_min, nav_off_max))
|
|
end
|
|
|
|
-- Convert position ranges to item index ranges
|
|
-- - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping.
|
|
-- - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list,
|
|
-- which with the flooring/ceiling tend to lead to 2 items instead of one being submitted.
|
|
for _, range in data.Ranges:iter() do
|
|
if range.PosToIndexConvert then
|
|
local m1 = ImTrunc((range.Min - window.DC.CursorPos.y - data.LossynessOffset) / clipper.ItemsHeight)
|
|
local m2 = ImTrunc(((range.Max - window.DC.CursorPos.y - data.LossynessOffset) / clipper.ItemsHeight) + 0.999999)
|
|
range.Min = ImClamp(already_submitted + m1 + range.PosToIndexOffsetMin, already_submitted, clipper.ItemsCount - 1)
|
|
range.Max = ImClamp(already_submitted + m2 + range.PosToIndexOffsetMax, range.Min + 1, clipper.ItemsCount)
|
|
range.PosToIndexConvert = false
|
|
end
|
|
end
|
|
ImGuiListClipper_SortAndFuseRanges(data.Ranges, data.StepNo)
|
|
end
|
|
|
|
-- Step 0+ (if item height is given in advance) or 1+: Display the next range in line.
|
|
while data.StepNo < data.Ranges.Size do
|
|
local r = data.Ranges.Data[data.StepNo + 1]
|
|
clipper.DisplayStart = ImMax(r.Min, already_submitted)
|
|
clipper.DisplayEnd = ImMin(r.Max, clipper.ItemsCount)
|
|
data.StepNo = data.StepNo + 1
|
|
if clipper.DisplayStart < clipper.DisplayEnd then
|
|
if clipper.DisplayStart > already_submitted then
|
|
clipper:SeekCursorForItem(clipper.DisplayStart)
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
-- After the last step: Let the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd),
|
|
-- Advance the cursor to the end of the list and then returns 'false' to end the loop.
|
|
if clipper.ItemsCount < INT_MAX then
|
|
clipper:SeekCursorForItem(clipper.ItemsCount)
|
|
end
|
|
return false
|
|
end
|
|
|
|
function MT.ImGuiListClipper:Step()
|
|
local ret = ImGuiListClipper_StepInternal(self)
|
|
if ret and (self.DisplayStart >= self.DisplayEnd) then
|
|
ret = false
|
|
end
|
|
if not ret then
|
|
self:End()
|
|
end
|
|
return ret
|
|
end
|
|
|
|
-- Generic helper, equivalent to old ImGui.CalcListClipping() but stateless
|
|
-- Returns (out_visible_start, out_visible_end)
|
|
function ImGui.CalcClipRectVisibleItemsY(clip_rect, pos, items_height)
|
|
local out_visible_start = ImMax(ImTrunc((clip_rect.Min.y - pos.y) / items_height), 0)
|
|
local out_visible_end = ImMax(ImTrunc(ImCeil((clip_rect.Max.y - pos.y) / items_height)), out_visible_start)
|
|
return out_visible_start, out_visible_end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] ERROR CHECKING, STATE RECOVERY
|
|
-------------------------------------------------------------------------------
|
|
-- - ErrorRecoveryStoreState()
|
|
-- - ErrorRecoveryTryToRecoverState()
|
|
-- - ErrorRecoveryTryToRecoverWindowState()
|
|
-- - ErrorLog()
|
|
-------------------------------------------------------------------------------
|
|
|
|
function ImGuiErrorRecoveryState()
|
|
return { SizeOfWindowStack = 0, SizeOfIDStack = 0, SizeOfTreeStack = 0, SizeOfColorStack = 0, SizeOfStyleVarStack = 0,
|
|
SizeOfFontStack = 0, SizeOfFocusScopeStack = 0, SizeOfGroupStack = 0, SizeOfItemFlagsStack = 0,
|
|
SizeOfBeginPopupStack = 0, SizeOfDisabledStack = 0 }
|
|
end
|
|
|
|
-- Save current stack sizes. Called e.g. by NewFrame() and by Begin() but may be called for manual recovery.
|
|
function ImGui.ErrorRecoveryStoreState(state_out)
|
|
local g = GImGui
|
|
state_out.SizeOfWindowStack = g.CurrentWindowStack.Size
|
|
state_out.SizeOfIDStack = g.CurrentWindow.IDStack.Size
|
|
state_out.SizeOfTreeStack = g.CurrentWindow.DC.TreeDepth -- NOT g.TreeNodeStack.Size which is a partial stack!
|
|
state_out.SizeOfColorStack = g.ColorStack.Size
|
|
state_out.SizeOfStyleVarStack = g.StyleVarStack.Size
|
|
state_out.SizeOfFontStack = g.FontStack.Size
|
|
state_out.SizeOfFocusScopeStack = g.FocusScopeStack.Size
|
|
state_out.SizeOfGroupStack = g.GroupStack.Size
|
|
state_out.SizeOfItemFlagsStack = g.ItemFlagsStack.Size
|
|
state_out.SizeOfBeginPopupStack = g.BeginPopupStack.Size
|
|
state_out.SizeOfDisabledStack = g.DisabledStackSize
|
|
end
|
|
|
|
-- Chosen name "Try to recover" over e.g. "Restore" to suggest this is not a 100% guaranteed recovery.
|
|
-- Called by e.g. EndFrame() but may be called for manual recovery.
|
|
-- Attempt to recover full window stack.
|
|
function ImGui.ErrorRecoveryTryToRecoverState(state_in)
|
|
local g = GImGui
|
|
while g.CurrentWindowStack.Size > state_in.SizeOfWindowStack do
|
|
-- Recap:
|
|
-- - Begin()/BeginChild() return false to indicate the window is collapsed or fully clipped.
|
|
-- - Always call a matching End() for each Begin() call, regardless of its return value!
|
|
-- - Begin/End and BeginChild/EndChild logic is KNOWN TO BE INCONSISTENT WITH ALL OTHER BEGIN/END FUNCTIONS.
|
|
-- - We will fix that in a future major update.
|
|
local window = g.CurrentWindow
|
|
if bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) ~= 0 then
|
|
if g.CurrentTable ~= nil and g.CurrentTable.InnerWindow == g.CurrentWindow then
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndTable()")
|
|
ImGui.EndTable()
|
|
else
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndChild()")
|
|
ImGui.EndChild()
|
|
end
|
|
else
|
|
IM_ASSERT_USER_ERROR(false, "Missing End()")
|
|
ImGui.End()
|
|
end
|
|
end
|
|
if g.CurrentWindowStack.Size == state_in.SizeOfWindowStack then
|
|
ImGui.ErrorRecoveryTryToRecoverWindowState(state_in)
|
|
end
|
|
end
|
|
|
|
-- Called by e.g. End() but may be called for manual recovery.
|
|
-- Attempt to recover from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls.
|
|
function ImGui.ErrorRecoveryTryToRecoverWindowState(state_in)
|
|
local g = GImGui
|
|
|
|
while g.CurrentTable ~= nil and g.CurrentTable.InnerWindow == g.CurrentWindow do
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndTable()")
|
|
ImGui.EndTable()
|
|
end
|
|
|
|
local window = g.CurrentWindow
|
|
|
|
-- FIXME: Can't recover from inside BeginTabItem/EndTabItem yet.
|
|
while g.CurrentTabBar ~= nil and g.CurrentTabBar.Window == window do
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndTabBar()")
|
|
ImGui.EndTabBar()
|
|
end
|
|
while g.CurrentMultiSelect ~= nil and g.CurrentMultiSelect.Storage.Window == window and ImGui.EndMultiSelect do
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndMultiSelect()")
|
|
ImGui.EndMultiSelect()
|
|
end
|
|
if window.DC.MenuBarAppending then
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndMenuBar()")
|
|
ImGui.EndMenuBar()
|
|
end
|
|
while window.DC.TreeDepth > state_in.SizeOfTreeStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing TreePop()")
|
|
ImGui.TreePop()
|
|
end
|
|
while g.GroupStack.Size > state_in.SizeOfGroupStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndGroup()")
|
|
ImGui.EndGroup()
|
|
end
|
|
IM_ASSERT(g.GroupStack.Size == state_in.SizeOfGroupStack)
|
|
while window.IDStack.Size > state_in.SizeOfIDStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing PopID()")
|
|
ImGui.PopID()
|
|
end
|
|
while g.DisabledStackSize > state_in.SizeOfDisabledStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing EndDisabled()")
|
|
if bit32.band(g.CurrentItemFlags, ImGuiItemFlags.Disabled) ~= 0 then
|
|
ImGui.EndDisabled()
|
|
else
|
|
ImGui.EndDisabledOverrideReenable()
|
|
g.CurrentWindowStack:back().DisabledOverrideReenable = false
|
|
end
|
|
end
|
|
IM_ASSERT(g.DisabledStackSize == state_in.SizeOfDisabledStack)
|
|
while g.ColorStack.Size > state_in.SizeOfColorStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing PopStyleColor()")
|
|
ImGui.PopStyleColor()
|
|
end
|
|
while g.ItemFlagsStack.Size > state_in.SizeOfItemFlagsStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing PopItemFlag()")
|
|
ImGui.PopItemFlag()
|
|
end
|
|
while g.StyleVarStack.Size > state_in.SizeOfStyleVarStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing PopStyleVar()")
|
|
ImGui.PopStyleVar()
|
|
end
|
|
while g.FontStack.Size > state_in.SizeOfFontStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing PopFont()")
|
|
ImGui.PopFont()
|
|
end
|
|
while g.FocusScopeStack.Size > state_in.SizeOfFocusScopeStack do
|
|
IM_ASSERT_USER_ERROR(false, "Missing PopFocusScope()")
|
|
ImGui.PopFocusScope()
|
|
end
|
|
-- IM_ASSERT(g.FocusScopeStack.Size == state_in.SizeOfFocusScopeStack)
|
|
end
|
|
|
|
-- Lines are stored in g.DebugLogBuf (one chunk per call)
|
|
function ImGui.DebugLog(fmt, ...)
|
|
local g = GImGui
|
|
g.DebugLogBuf = g.DebugLogBuf or ImGuiTextBuffer()
|
|
local str = string.format("[%05d] ", g.FrameCount) .. ((select("#", ...) > 0) and string.format(fmt, ...) or fmt)
|
|
g.DebugLogBuf:append(str)
|
|
-- Line index (upstream: g.DebugLogIndex). Lines are complete strings without '\n'.
|
|
local lines = g.DebugLogLines
|
|
if lines == nil then lines = { "" }; g.DebugLogLines = lines end
|
|
for part, nl in string.gmatch(str, "([^\n]*)(\n?)") do
|
|
lines[#lines] = lines[#lines] .. part
|
|
if nl ~= "" then lines[#lines + 1] = "" end
|
|
if part == "" and nl == "" then break end
|
|
end
|
|
if bit32.band(g.DebugLogFlags, ImGuiDebugLogFlags.OutputToTTY) ~= 0 then
|
|
print((string.gsub(str, "\n$", "")))
|
|
end
|
|
end
|
|
|
|
--- @return bool should_assert
|
|
function ImGui.ErrorLog(msg)
|
|
local g = GImGui
|
|
if g == nil then return true end
|
|
-- Output to debug log
|
|
local window = g.CurrentWindow
|
|
|
|
if g.IO.ConfigErrorRecoveryEnableDebugLog then
|
|
if g.ErrorFirst then
|
|
ImGui.DebugLog(string.format("[imgui-error] (current settings: Assert=%d, Log=%d, Tooltip=%d)\n",
|
|
g.IO.ConfigErrorRecoveryEnableAssert and 1 or 0, g.IO.ConfigErrorRecoveryEnableDebugLog and 1 or 0, g.IO.ConfigErrorRecoveryEnableTooltip and 1 or 0))
|
|
end
|
|
ImGui.DebugLog(string.format("[imgui-error] In window '%s': %s\n", window and window.Name or "NULL", tostring(msg)))
|
|
end
|
|
g.ErrorFirst = false
|
|
|
|
-- Output to tooltip
|
|
if g.IO.ConfigErrorRecoveryEnableTooltip then
|
|
if g.WithinFrameScope and ImGui.BeginErrorTooltip() then
|
|
if g.ErrorCountCurrentFrame < 20 then
|
|
ImGui.Text("In window '%s': %s", window and window.Name or "NULL", tostring(msg))
|
|
if window and (not window.IsFallbackWindow or window.WasActive) then
|
|
ImGui.GetForegroundDrawList(window.Viewport):AddRect(window.Pos, window.Pos + window.Size, IM_COL32(255, 0, 0, 255))
|
|
end
|
|
end
|
|
if g.ErrorCountCurrentFrame == 20 then
|
|
ImGui.Text("(and more errors)")
|
|
end
|
|
-- EndFrame() will amend debug buttons to this window, after all errors have been submitted.
|
|
ImGui.EndErrorTooltip()
|
|
end
|
|
g.ErrorCountCurrentFrame = g.ErrorCountCurrentFrame + 1
|
|
end
|
|
|
|
-- Output to callback
|
|
if g.ErrorCallback ~= nil then
|
|
g.ErrorCallback(g, g.ErrorCallbackUserData, msg)
|
|
end
|
|
|
|
-- Return whether we should assert
|
|
return g.IO.ConfigErrorRecoveryEnableAssert
|
|
end
|
|
|
|
-- Display an error tooltip when same ID as HoveredId was submitted multiple times.
|
|
-- See code in ItemHoverable() for an explanation of why we associate this error to HoveredId + code drawing of rectangles over individual items instances.
|
|
function ImGui.ErrorCheckEndFrameFinalizeErrorTooltip()
|
|
local g = GImGui
|
|
if g.DebugDrawIdConflictsId ~= 0 and g.IO.KeyCtrl == false then
|
|
g.DebugDrawIdConflictsCount = g.HoveredIdPreviousFrameItemCount
|
|
end
|
|
if g.DebugDrawIdConflictsId ~= 0 and not g.DebugItemPickerActive and ImGui.BeginErrorTooltip() then
|
|
ImGui.Text("Programmer error: %d visible items with conflicting ID!", g.DebugDrawIdConflictsCount)
|
|
ImGui.BulletText("Code should use PushID()/PopID() in loops, or append \"##xx\" to same-label identifiers!")
|
|
ImGui.BulletText("Empty label e.g. Button(\"\") == same ID as parent widget/node. Use Button(\"##xx\") instead!")
|
|
-- ImGui.BulletText("Code intending to use duplicate ID may use e.g. PushItemFlag(ImGuiItemFlags.AllowDuplicateId, true); ... PopItemFlag()") -- Not making this too visible for fear of it being abused.
|
|
ImGui.BulletText("Set io.ConfigDebugHighlightIdConflicts=false to disable this warning in non-programmers builds.")
|
|
ImGui.Separator()
|
|
ImGui.Text("(Hold Ctrl to: ")
|
|
ImGui.SameLine(0.0, 0.0)
|
|
ImGui.TextLinkOpenURL("read FAQ \"About ID Stack System\"", "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage")
|
|
ImGui.SameLine(0.0, 0.0)
|
|
ImGui.Text(")")
|
|
ImGui.EndErrorTooltip()
|
|
end
|
|
|
|
if g.ErrorCountCurrentFrame > 0 and ImGui.BeginErrorTooltip() then -- Amend at end of frame
|
|
ImGui.Separator()
|
|
ImGui.Text("(Hold Ctrl to: ")
|
|
ImGui.SameLine(0.0, 0.0)
|
|
if ImGui.SmallButton("Enable Asserts") then
|
|
g.IO.ConfigErrorRecoveryEnableAssert = true
|
|
end
|
|
-- ImGui.SameLine()
|
|
-- if ImGui.SmallButton("Hide Error Tooltips") then
|
|
-- g.IO.ConfigErrorRecoveryEnableTooltip = false -- Too dangerous
|
|
-- end
|
|
ImGui.SameLine(0, 0)
|
|
ImGui.Text(")")
|
|
ImGui.EndErrorTooltip()
|
|
end
|
|
end
|
|
|
|
-- Pseudo-tooltip. Follow mouse until Ctrl is held. When Ctrl is held we lock position, allowing to click it.
|
|
function ImGui.BeginErrorTooltip()
|
|
local g = GImGui
|
|
local window = ImGui.FindWindowByName("##Tooltip_Error")
|
|
local use_locked_pos = g.IO.KeyCtrl and window ~= nil and window.WasActive
|
|
local c = g.Style.Colors[ImGuiCol.PopupBg]
|
|
ImGui.PushStyleColor(ImGuiCol.PopupBg, ImVec4(ImLerp(c.x, 1.0, 0.15), ImLerp(c.y, 0.0, 0.15), ImLerp(c.z, 0.0, 0.15), ImLerp(c.w, 1.0, 0.15)))
|
|
if use_locked_pos then
|
|
ImGui.SetNextWindowPos(g.ErrorTooltipLockedPos)
|
|
end
|
|
local _, is_visible = ImGui.Begin("##Tooltip_Error", nil, bit32.bor(ImGuiWindowFlags.Tooltip, ImGuiWindowFlags.NoDecoration, ImGuiWindowFlags.NoMove, ImGuiWindowFlags.NoResize, ImGuiWindowFlags.NoSavedSettings, ImGuiWindowFlags.AlwaysAutoResize))
|
|
ImGui.PopStyleColor()
|
|
if is_visible and g.CurrentWindow.BeginCount == 1 then
|
|
ImGui.SeparatorText("MESSAGE FROM DEAR IMGUI")
|
|
ImGui.BringWindowToDisplayFront(g.CurrentWindow)
|
|
ImGui.BringWindowToFocusFront(g.CurrentWindow)
|
|
g.ErrorTooltipLockedPos = ImGui.GetWindowPos()
|
|
elseif not is_visible then
|
|
ImGui.End()
|
|
end
|
|
return is_visible
|
|
end
|
|
|
|
function ImGui.EndErrorTooltip()
|
|
ImGui.End()
|
|
end
|
|
|
|
|
|
do --[[ [SECTION] METRICS/DEBUGGER WINDOW, DEBUG LOG, ID STACK TOOL, FONT DEBUG ]]
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] Font atlas / font debugging
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- byte buffers for InputText (same as the demo's DemoBuf/DemoBufStr; kept here so the library doesn't need the demo)
|
|
local function DemoBuf(str, size)
|
|
local t = {}
|
|
for i = 1, #str do t[i] = string.byte(str, i) end
|
|
t[#str + 1] = 0
|
|
if size then for i = #str + 2, size do t[i] = 0 end end
|
|
return t
|
|
end
|
|
local function DemoBufStr(buf)
|
|
local n = 0
|
|
while buf[n + 1] ~= nil and buf[n + 1] ~= 0 do n = n + 1 end
|
|
if n == 0 then return "" end
|
|
return ImGui._ByteArrayToString(buf, 1, n + 1)
|
|
end
|
|
|
|
-- Avoid naming collision with imgui_demo's HelpMarker() for unity builds.
|
|
local function MetricsHelpMarker(desc)
|
|
ImGui.TextDisabled("(?)")
|
|
if ImGui.BeginItemTooltip() then
|
|
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35.0)
|
|
ImGui.TextUnformatted(desc)
|
|
ImGui.PopTextWrapPos()
|
|
ImGui.EndTooltip()
|
|
end
|
|
end
|
|
ImGui.MetricsHelpMarker = MetricsHelpMarker
|
|
|
|
local function U8(c) return (c and c > 0) and utf8.char(c) or "" end
|
|
|
|
-- Demo helper function to select among loaded fonts.
|
|
-- Here we use the regular BeginCombo()/EndCombo() api which is the more flexible one.
|
|
function ImGui.ShowFontSelector(label)
|
|
local io = ImGui.GetIO()
|
|
local font_current = ImGui.GetFont()
|
|
if ImGui.BeginCombo(label, font_current:GetDebugName()) then
|
|
for i, font in io.Fonts.Fonts:iter() do
|
|
ImGui.PushID(i)
|
|
if ImGui.Selectable(font:GetDebugName(), font == font_current, ImGuiSelectableFlags.SelectOnNav) then io.FontDefault = font end
|
|
if font == font_current then ImGui.SetItemDefaultFocus() end
|
|
ImGui.PopID()
|
|
end
|
|
ImGui.EndCombo()
|
|
end
|
|
ImGui.SameLine()
|
|
MetricsHelpMarker("- Load additional fonts with io.Fonts->AddFontXXX() functions.\n- On Roblox: register the .ttf bytes with ImGui_ImplRoblox.AddFile() first.\n- Read FAQ and docs/FONTS.md for more details.")
|
|
end
|
|
|
|
function ImGui.DebugNodeFontGlyph(font, glyph)
|
|
ImGui.Text("Codepoint: U+%04X", glyph.Codepoint)
|
|
ImGui.Separator()
|
|
ImGui.Text("Visible: %d", glyph.Visible and 1 or 0)
|
|
ImGui.Text("AdvanceX: %.1f", glyph.AdvanceX)
|
|
ImGui.Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph.X0, glyph.Y0, glyph.X1, glyph.Y1)
|
|
ImGui.Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph.U0, glyph.V0, glyph.U1, glyph.V1)
|
|
if glyph.PackId >= 0 then
|
|
local r = { uv0 = ImVec2(), uv1 = ImVec2() }
|
|
if font.OwnerAtlas:GetCustomRect(glyph.PackId, r) then ImGui.Text("PackId: 0x%X (%dx%d rect at %d,%d)", glyph.PackId, r.w, r.h, r.x, r.y) end
|
|
end
|
|
ImGui.Text("SourceIdx: %d", glyph.SourceIdx)
|
|
end
|
|
|
|
function ImGui.DebugNodeFontGlyphsForSrcMask(font, baked, src_mask)
|
|
local draw_list = ImGui.GetWindowDrawList()
|
|
local glyph_col = ImGui.GetColorU32(ImGuiCol.Text)
|
|
local cell_size = baked.Size * 1
|
|
local cell_spacing = ImGui.GetStyle().ItemSpacing.y
|
|
-- [port] only scans loaded codepoints (IndexLookup range) instead of the whole 0..0xFFFF range
|
|
local max_c = baked.IndexLookup.Size - 1
|
|
local base = 0
|
|
while base <= max_c do
|
|
local count = 0
|
|
for n = 0, 255 do
|
|
local c = base + n
|
|
if baked:IsGlyphLoaded(c) then
|
|
local glyph = baked:FindGlyphNoFallback(c)
|
|
if glyph and bit32.band(src_mask, bit32.lshift(1, glyph.SourceIdx)) ~= 0 then count = count + 1 end
|
|
end
|
|
end
|
|
if count > 0 and ImGui.TreeNode(base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, (count > 1) and "glyphs" or "glyph") then
|
|
-- Draw a 16x16 grid of glyphs
|
|
local base_pos = ImGui.GetCursorScreenPos()
|
|
for n = 0, 255 do
|
|
local c = base + n
|
|
-- We use ImFont:RenderChar as a shortcut because we don't have UTF-8 conversion functions
|
|
-- available here and thus cannot easily generate a zero-terminated UTF-8 encoded string.
|
|
local cell_p1 = ImVec2(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + math.floor(n / 16) * (cell_size + cell_spacing))
|
|
local cell_p2 = ImVec2(cell_p1.x + cell_size, cell_p1.y + cell_size)
|
|
local glyph = baked:IsGlyphLoaded(c) and baked:FindGlyphNoFallback(c) or nil
|
|
draw_list:AddRect(cell_p1, cell_p2, glyph and IM_COL32(255, 255, 255, 100) or IM_COL32(255, 255, 255, 50))
|
|
if glyph and bit32.band(src_mask, bit32.lshift(1, glyph.SourceIdx)) ~= 0 then
|
|
font:RenderChar(draw_list, cell_size, cell_p1, glyph_col, c)
|
|
if ImGui.IsMouseHoveringRect(cell_p1, cell_p2) and ImGui.BeginTooltip() then
|
|
ImGui.DebugNodeFontGlyph(font, glyph)
|
|
ImGui.EndTooltip()
|
|
end
|
|
end
|
|
end
|
|
ImGui.Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16))
|
|
ImGui.TreePop()
|
|
end
|
|
base = base + 256
|
|
end
|
|
end
|
|
|
|
-- [DEBUG] Display details for a single font, called by ShowStyleEditor().
|
|
function ImGui.DebugNodeFont(font)
|
|
local g = ImGui.GetCurrentContext()
|
|
local cfg = g.DebugMetricsConfig
|
|
local atlas = font.OwnerAtlas
|
|
local opened = ImGui.TreeNode(tostring(font), "Font: \"%s\": %d sources(s)", font:GetDebugName(), font.Sources.Size)
|
|
|
|
-- Display preview text
|
|
if not opened then ImGui.Indent() end
|
|
ImGui.Indent()
|
|
if cfg.ShowFontPreview then
|
|
ImGui.PushFont(font, 0.0)
|
|
ImGui.Text("The quick brown fox jumps over the lazy dog")
|
|
ImGui.PopFont()
|
|
end
|
|
if not opened then
|
|
ImGui.Unindent()
|
|
ImGui.Unindent()
|
|
return
|
|
end
|
|
if ImGui.SmallButton("Set as default") then ImGui.GetIO().FontDefault = font end
|
|
ImGui.SameLine()
|
|
ImGui.BeginDisabled(atlas.Fonts.Size <= 1 or atlas.Locked == true or atlas.RemoveFont == nil)
|
|
if ImGui.SmallButton("Remove") and atlas.RemoveFont then atlas:RemoveFont(font) end
|
|
ImGui.EndDisabled()
|
|
ImGui.SameLine()
|
|
if ImGui.SmallButton("Clear bakes") then ImFontAtlasFontDiscardBakes(atlas, font, 0) end
|
|
ImGui.SameLine()
|
|
if ImGui.SmallButton("Clear unused") then ImFontAtlasFontDiscardBakes(atlas, font, 2) end
|
|
|
|
-- Display details
|
|
ImGui.Text("Fallback character: '%s' (U+%04X)", U8(font.FallbackChar), font.FallbackChar or 0)
|
|
ImGui.Text("Ellipsis character: '%s' (U+%04X)", U8(font.EllipsisChar), font.EllipsisChar or 0)
|
|
for src_n, src in font.Sources:iter() do
|
|
if ImGui.TreeNode(tostring(src), "Input %d: '%s' [%d], Oversample: %d,%d, PixelSnapH: %d, Offset: (%.1f,%.1f)",
|
|
src_n - 1, src.Name or "", src.FontNo or 0, src.OversampleH or 0, src.OversampleV or 0, src.PixelSnapH and 1 or 0, src.GlyphOffset.x, src.GlyphOffset.y) then
|
|
local loader = src.FontLoader or atlas.FontLoader
|
|
ImGui.Text("Loader: '%s'", (loader and loader.Name) or "N/A")
|
|
|
|
-- if ImGui.DragFloat("ExtraSizeScale", src.ExtraSizeScale, 0.01, 0.10, 2.0) then
|
|
-- ImFontAtlasFontRebuildOutput(atlas, font)
|
|
-- end
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
|
|
-- Display all glyphs of the fonts in separate pages of 256 characters
|
|
local builder = atlas.Builder
|
|
if builder then
|
|
for _, baked in builder.BakedPool:iter() do
|
|
if baked.OwnerFont == font then
|
|
ImGui.PushID(baked.BakedId)
|
|
if ImGui.TreeNode("Glyphs", "Baked at { %.2fpx, d.%.2f }: %d glyphs%s", baked.Size, baked.RasterizerDensity, baked.Glyphs.Size,
|
|
(baked.LastUsedFrame < (builder.FrameCount or 0) - 1) and " *Unused*" or "") then
|
|
if ImGui.SmallButton("Load all") then
|
|
for c = 0, 0x7F do baked:FindGlyph(c) end -- [port] ASCII only: loading the whole BMP in Luau stalls for seconds
|
|
end
|
|
local surface_sqrt = math.floor(math.sqrt(baked.MetricsTotalSurface))
|
|
ImGui.Text("Ascent: %f, Descent: %f, Ascent-Descent: %f", baked.Ascent, baked.Descent, baked.Ascent - baked.Descent)
|
|
ImGui.Text("Texture Area: about %d px ~%dx%d px", baked.MetricsTotalSurface, surface_sqrt, surface_sqrt)
|
|
for src_n, src in font.Sources:iter() do
|
|
ImGui.BulletText("Input %d: '%s', Oversample: (%d,%d), PixelSnapH: %d, Offset: (%.1f,%.1f)",
|
|
src_n - 1, src.Name or "", src.OversampleH or 0, src.OversampleV or 0, src.PixelSnapH and 1 or 0, src.GlyphOffset.x, src.GlyphOffset.y)
|
|
end
|
|
ImGui.DebugNodeFontGlyphsForSrcMask(font, baked, bit32.bnot(0))
|
|
ImGui.TreePop()
|
|
end
|
|
ImGui.PopID()
|
|
end
|
|
end
|
|
end
|
|
ImGui.TreePop()
|
|
ImGui.Unindent()
|
|
end
|
|
|
|
local StatusNames = { [0] = "OK", "Destroyed", "WantCreate", "WantUpdates", "WantDestroy" }
|
|
local FormatNames = { [0] = "RGBA32", "Alpha8" }
|
|
|
|
function ImGui.DebugNodeTexture(tex, int_id, highlight_rect)
|
|
local g = ImGui.GetCurrentContext()
|
|
ImGui.PushID(int_id)
|
|
if ImGui.TreeNode("", "Texture #%03d (%dx%d pixels)", tex.UniqueID or 0, tex.Width, tex.Height) then
|
|
local cfg = g.DebugMetricsConfig
|
|
_, cfg.ShowTextureUsedRect = ImGui.Checkbox("Show used rect", cfg.ShowTextureUsedRect)
|
|
ImGui.PushStyleVar(ImGuiStyleVar.ImageBorderSize, math.max(1.0, g.Style.ImageBorderSize))
|
|
local p = ImGui.GetCursorScreenPos()
|
|
if tex.Status == ImTextureStatus.WantDestroy or tex.Status == ImTextureStatus.Destroyed then
|
|
ImGui.Dummy(ImVec2(tex.Width, tex.Height))
|
|
else
|
|
local ref = ImTextureRef(); ref._TexData = tex
|
|
ImGui.ImageWithBg(ref, ImVec2(tex.Width, tex.Height), ImVec2(0.0, 0.0), ImVec2(1.0, 1.0), ImVec4(0.0, 0.0, 0.0, 1.0))
|
|
end
|
|
if cfg.ShowTextureUsedRect and tex.UsedRect then
|
|
local u = tex.UsedRect
|
|
ImGui.GetWindowDrawList():AddRect(ImVec2(p.x + u.x, p.y + u.y), ImVec2(p.x + u.x + u.w, p.y + u.y + u.h), IM_COL32(255, 0, 255, 255))
|
|
end
|
|
if highlight_rect ~= nil then
|
|
local r_outer = ImRect(p.x, p.y, p.x + tex.Width, p.y + tex.Height)
|
|
local r_inner = ImRect(p.x + highlight_rect.x, p.y + highlight_rect.y, p.x + highlight_rect.x + highlight_rect.w, p.y + highlight_rect.y + highlight_rect.h)
|
|
ImGui.RenderRectFilledWithHole(ImGui.GetWindowDrawList(), r_outer, r_inner, IM_COL32(0, 0, 0, 100), 0.0)
|
|
ImGui.GetWindowDrawList():AddRect(r_inner.Min - ImVec2(1, 1), r_inner.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255))
|
|
end
|
|
ImGui.PopStyleVar()
|
|
ImGui.Text("Status = %s (%d), Format = %s (%d), UseColors = %d", StatusNames[tex.Status] or "?", tex.Status, FormatNames[tex.Format] or "?", tex.Format, tex.UseColors and 1 or 0)
|
|
ImGui.Text("TexID = %s", tostring(tex.TexID))
|
|
ImGui.TreePop()
|
|
end
|
|
ImGui.PopID()
|
|
end
|
|
|
|
-- [DEBUG] List fonts in a font atlas and display its texture
|
|
function ImGui.ShowFontAtlas(atlas)
|
|
local g = ImGui.GetCurrentContext()
|
|
local io = g.IO
|
|
local style = g.Style
|
|
ImGui.BeginDisabled()
|
|
ImGui.CheckboxFlags("io.BackendFlags: RendererHasTextures", io.BackendFlags, ImGuiBackendFlags.RendererHasTextures)
|
|
ImGui.EndDisabled()
|
|
ImGui.ShowFontSelector("Font")
|
|
local changed
|
|
style.FontSizeBase, changed = ImGui.DragFloat("FontSizeBase", style.FontSizeBase, 0.20, 5.0, 100.0, "%.0f")
|
|
if changed then style._NextFrameFontSizeBase = style.FontSizeBase end -- FIXME: Temporary hack until we finish remaining work.
|
|
ImGui.SameLine(0.0, 0.0); ImGui.Text(" (out %.2f)", ImGui.GetFontSize())
|
|
ImGui.SameLine(); MetricsHelpMarker("- This is scaling font only. General scaling will come later.")
|
|
style.FontScaleMain = ImGui.DragFloat("FontScaleMain", style.FontScaleMain, 0.02, 0.5, 4.0)
|
|
style.FontScaleDpi = ImGui.DragFloat("FontScaleDpi", style.FontScaleDpi, 0.02, 0.5, 4.0)
|
|
ImGui.BulletText("Load a nice font for better results!")
|
|
ImGui.BulletText("Please submit feedback:")
|
|
ImGui.SameLine(); ImGui.TextLinkOpenURL("#8465", "https://github.com/ocornut/imgui/issues/8465")
|
|
ImGui.BulletText("Read FAQ for more details:")
|
|
ImGui.SameLine(); ImGui.TextLinkOpenURL("dearimgui.com/faq", "https://www.dearimgui.com/faq/")
|
|
|
|
ImGui.SeparatorText("Font List")
|
|
local cfg = g.DebugMetricsConfig
|
|
_, cfg.ShowFontPreview = ImGui.Checkbox("Show font preview", cfg.ShowFontPreview == true)
|
|
-- Font loaders
|
|
if ImGui.TreeNode("Loader", "Loader: '%s'", atlas.FontLoaderName or "NULL") then
|
|
ImGui.BeginDisabled()
|
|
ImGui.RadioButtonEx("stb_truetype", true)
|
|
ImGui.SameLine()
|
|
ImGui.RadioButtonEx("FreeType", false)
|
|
ImGui.SetItemTooltip("Not available on Roblox.")
|
|
ImGui.EndDisabled()
|
|
ImGui.TreePop()
|
|
end
|
|
-- Font list
|
|
for i, font in atlas.Fonts:iter() do
|
|
ImGui.PushID(i)
|
|
ImGui.DebugNodeFont(font)
|
|
ImGui.PopID()
|
|
end
|
|
|
|
ImGui.SeparatorText("Font Atlas")
|
|
if ImGui.Button("Compact") then atlas:CompactCache() end
|
|
ImGui.SameLine()
|
|
if ImGui.Button("Grow") then ImFontAtlasTextureGrow(atlas) end
|
|
ImGui.SameLine()
|
|
if ImGui.Button("Clear All") then ImFontAtlasBuildClear(atlas) end
|
|
ImGui.SetItemTooltip("Destroy cache and custom rectangles.")
|
|
for tex_n, tex in atlas.TexList:iter() do
|
|
if tex_n > 1 then ImGui.SameLine() end
|
|
ImGui.Text("Tex: %dx%d", tex.Width, tex.Height)
|
|
end
|
|
local b = atlas.Builder
|
|
if b then
|
|
local packed_sqrt = math.floor(math.sqrt(b.RectsPackedSurface or 0))
|
|
local discarded_sqrt = math.floor(math.sqrt(b.RectsDiscardedSurface or 0))
|
|
ImGui.Text("Packed rects: %d, area: about %d px ~%dx%d px", b.RectsPackedCount or 0, b.RectsPackedSurface or 0, packed_sqrt, packed_sqrt)
|
|
ImGui.Text("incl. Discarded rects: %d, area: about %d px ~%dx%d px", b.RectsDiscardedCount or 0, b.RectsDiscardedSurface or 0, discarded_sqrt, discarded_sqrt)
|
|
end
|
|
-- Texture list
|
|
-- (ensure the last texture always use the same ID, so we can keep it open neatly)
|
|
for tex_n, tex in atlas.TexList:iter() do
|
|
if tex_n == atlas.TexList.Size then ImGui.SetNextItemOpen(true, ImGuiCond.Once) end
|
|
ImGui.DebugNodeTexture(tex, atlas.TexList.Size - tex_n, nil)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] DEBUG LOG WINDOW
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- FIXME-LAYOUT: To be done automatically via layout mode once we rework ItemSize/ItemAdd into ItemLayout.
|
|
local function SameLineOrWrap(size)
|
|
local g = ImGui.GetCurrentContext()
|
|
local window = g.CurrentWindow
|
|
local pos = ImVec2(window.DC.CursorPosPrevLine.x + g.Style.ItemSpacing.x, window.DC.CursorPosPrevLine.y)
|
|
if window.WorkRect:Contains(ImRect(pos, pos + size)) then ImGui.SameLine() end
|
|
end
|
|
|
|
local function ShowDebugLogFlag(name, flags)
|
|
local g = ImGui.GetCurrentContext()
|
|
local size = ImVec2(ImGui.GetFrameHeight() + g.Style.ItemInnerSpacing.x + ImGui.CalcTextSize(name).x, ImGui.GetFrameHeight())
|
|
SameLineOrWrap(size) -- FIXME-LAYOUT: To be done automatically once we rework ItemSize/ItemAdd into ItemLayout.
|
|
local pressed
|
|
pressed, g.DebugLogFlags = ImGui.CheckboxFlags(name, g.DebugLogFlags, flags)
|
|
if pressed and g.IO.KeyShift and bit32.band(g.DebugLogFlags, flags) ~= 0 then
|
|
g.DebugLogAutoDisableFrames = 2
|
|
g.DebugLogAutoDisableFlags = bit32.bor(g.DebugLogAutoDisableFlags or 0, flags)
|
|
end
|
|
ImGui.SetItemTooltip("Hold Shift when clicking to enable for 2 frames only (useful for spammy log entries)")
|
|
end
|
|
|
|
function ImGui.ShowDebugLogWindow(p_open)
|
|
local g = ImGui.GetCurrentContext()
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSize) == 0 then
|
|
ImGui.SetNextWindowSize(ImVec2(0.0, ImGui.GetFontSize() * 12.0), ImGuiCond.FirstUseEver)
|
|
end
|
|
local visible
|
|
p_open, visible = ImGui.Begin("Dear ImGui Debug Log", p_open)
|
|
if not visible or ImGui.GetCurrentWindow().BeginCount > 1 then
|
|
ImGui.End()
|
|
return p_open
|
|
end
|
|
local L = ImGuiDebugLogFlags
|
|
local all_enable_flags = bit32.band(L.EventMask_, bit32.bnot(L.EventInputRouting))
|
|
_, g.DebugLogFlags = ImGui.CheckboxFlags("All", g.DebugLogFlags, all_enable_flags)
|
|
ImGui.SetItemTooltip("(except InputRouting which is spammy)")
|
|
for _, e in ipairs({ { "Errors", L.EventError }, { "ActiveId", L.EventActiveId }, { "Clipper", L.EventClipper }, { "Docking", L.EventDocking },
|
|
{ "Focus", L.EventFocus }, { "IO", L.EventIO }, { "Font", L.EventFont }, { "Nav", L.EventNav }, { "Popup", L.EventPopup },
|
|
{ "Selection", L.EventSelection }, { "Table", L.EventTable }, { "Viewport", L.EventViewport }, { "InputRouting", L.EventInputRouting } }) do
|
|
ShowDebugLogFlag(e[1], e[2])
|
|
end
|
|
if ImGui.SmallButton("Clear") then
|
|
if g.DebugLogBuf then g.DebugLogBuf:clear() end
|
|
g.DebugLogLines = { "" }
|
|
end
|
|
ImGui.SameLine()
|
|
if ImGui.SmallButton("Copy") then ImGui.SetClipboardText(g.DebugLogBuf and g.DebugLogBuf:c_str() or "") end
|
|
ImGui.SameLine()
|
|
if ImGui.SmallButton("Configure Outputs..") then ImGui.OpenPopup("Outputs") end
|
|
if ImGui.BeginPopup("Outputs") then
|
|
_, g.DebugLogFlags = ImGui.CheckboxFlags("OutputToTTY", g.DebugLogFlags, L.OutputToTTY)
|
|
ImGui.BeginDisabled()
|
|
_, g.DebugLogFlags = ImGui.CheckboxFlags("OutputToDebugger", g.DebugLogFlags, L.OutputToDebugger)
|
|
_, g.DebugLogFlags = ImGui.CheckboxFlags("OutputToTestEngine", g.DebugLogFlags, L.OutputToTestEngine)
|
|
ImGui.EndDisabled()
|
|
ImGui.EndPopup()
|
|
end
|
|
|
|
ImGui.BeginChild("##log", ImVec2(0.0, 0.0), ImGuiChildFlags.Borders, bit32.bor(ImGuiWindowFlags.AlwaysVerticalScrollbar, ImGuiWindowFlags.AlwaysHorizontalScrollbar))
|
|
local backup_log_flags = g.DebugLogFlags
|
|
g.DebugLogFlags = bit32.band(g.DebugLogFlags, bit32.bnot(L.EventClipper))
|
|
local lines = g.DebugLogLines or { "" }
|
|
local count = #lines
|
|
if lines[count] == "" then count = count - 1 end -- trailing partial line
|
|
local clipper = ImGuiListClipper()
|
|
clipper:Begin(count)
|
|
while clipper:Step() do
|
|
for line_no = clipper.DisplayStart, clipper.DisplayEnd - 1 do ImGui.TextUnformatted(lines[line_no + 1]) end
|
|
end
|
|
g.DebugLogFlags = backup_log_flags
|
|
if ImGui.GetScrollY() >= ImGui.GetScrollMaxY() then ImGui.SetScrollHereY(1.0) end
|
|
ImGui.EndChild()
|
|
ImGui.End()
|
|
return p_open
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL)
|
|
-------------------------------------------------------------------------------
|
|
|
|
local function PathQuery(g)
|
|
local q = g.DebugItemPathQuery
|
|
if q == nil then
|
|
q = { MainID = 0, Active = false, Complete = false, Step = -1, Results = {} }
|
|
g.DebugItemPathQuery = q
|
|
g.DebugIDStackTool = { LastActiveFrame = -1, OptHexEncodeNonAsciiChars = true, OptCopyToClipboardOnCtrlC = false, CopyToClipboardLastTime = -FLT_MAX }
|
|
end
|
|
return q
|
|
end
|
|
|
|
-- Update queries. The steps are: -1: query Stack, >= 0: query each stack item
|
|
-- We can only perform 1 ID Info query every frame. This is designed so the GetID() tests are cheap and constant-time
|
|
local function DebugItemPathQuery_UpdateAndGetHookId(query, id)
|
|
-- Update query. Clear hook when no active query
|
|
if query.MainID ~= id then
|
|
query.MainID = id
|
|
query.Step = -1
|
|
query.Complete = false
|
|
query.Results = {}
|
|
end
|
|
query.Active = false
|
|
if id == 0 then return 0 end
|
|
local res = query.Results
|
|
-- Advance to next stack level when we got our result, or after 2 frames (in case we never get a result)
|
|
if query.Step >= 0 and query.Step < #res then
|
|
if res[query.Step + 1].QuerySuccess or res[query.Step + 1].QueryFrameCount > 2 then query.Step = query.Step + 1 end
|
|
end
|
|
-- Update status and hook
|
|
query.Complete = (query.Step == #res)
|
|
if query.Step == -1 then
|
|
query.Active = true
|
|
return id
|
|
elseif query.Step >= 0 and query.Step < #res then
|
|
res[query.Step + 1].QueryFrameCount = res[query.Step + 1].QueryFrameCount + 1
|
|
query.Active = true
|
|
return res[query.Step + 1].ID
|
|
end
|
|
return 0
|
|
end
|
|
|
|
-- [DEBUG] ID Stack Tool: update query. Called by NewFrame()
|
|
function ImGui.UpdateDebugToolItemPathQuery()
|
|
local g = ImGui.GetCurrentContext()
|
|
local q = PathQuery(g)
|
|
local id = 0
|
|
if g.DebugIDStackTool.LastActiveFrame + 1 == g.FrameCount then
|
|
id = (g.HoveredIdPreviousFrame ~= 0 and g.HoveredIdPreviousFrame) or g.ActiveId
|
|
end
|
|
g.DebugHookIdInfoId = DebugItemPathQuery_UpdateAndGetHookId(q, id)
|
|
end
|
|
|
|
-- [DEBUG] ID Stack tool: hooks called by GetID() family functions
|
|
-- `data_id` is the string/number that was hashed.
|
|
function ImGui.DebugHookIdInfo(id, data_type, data_id)
|
|
local g = ImGui.GetCurrentContext()
|
|
local query = PathQuery(g)
|
|
if not query.Active then return end
|
|
local window = g.CurrentWindow
|
|
|
|
-- Step -1: stack query
|
|
-- This assumes that the ID was computed with the current ID stack, which tends to be the case for our widget.
|
|
if query.Step == -1 then
|
|
query.Step = 0
|
|
query.Results = {}
|
|
for n = 0, window.IDStack.Size do
|
|
query.Results[n + 1] = { ID = (n < window.IDStack.Size) and window.IDStack.Data[n + 1] or id, QueryFrameCount = 0, QuerySuccess = false, DataType = -1, Desc = nil }
|
|
end
|
|
return
|
|
end
|
|
-- Step 0+: query for individual level
|
|
if query.Step ~= window.IDStack.Size then return end
|
|
local info = query.Results[query.Step + 1]
|
|
if info == nil or info.ID ~= id then return end
|
|
if info.Desc == nil then
|
|
if data_type == ImGuiDataType.S32 then info.Desc = string.format("%d", data_id)
|
|
elseif data_type == ImGuiDataType.String then info.Desc = tostring(data_id)
|
|
else info.Desc = string.format("0x%08X [override]", id) end -- PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one.
|
|
end
|
|
info.QuerySuccess = true
|
|
if info.DataType == -1 then info.DataType = data_type end
|
|
end
|
|
|
|
local function SkipUncontributingPrefix(label)
|
|
local p = string.find(label, "###", 1, true)
|
|
if p then return string.sub(label, p) end
|
|
return label
|
|
end
|
|
|
|
local function FormatLevelInfo(g, query, n, format_for_ui)
|
|
local info = query.Results[n + 1]
|
|
local window = (info.Desc == nil and n == 0) and ImGui.FindWindowByID(info.ID) or nil
|
|
if window then return string.format(format_for_ui and "\"%s\" [window]" or "%s", SkipUncontributingPrefix(window.Name)) end -- Source: window name (because the root ID don't call GetID() and so doesn't get hooked)
|
|
if info.QuerySuccess then return string.format((format_for_ui and info.DataType == ImGuiDataType.String) and "\"%s\"" or "%s", SkipUncontributingPrefix(info.Desc)) end -- Source: GetID() hooks (prioritize over ItemInfo() because we frequently use patterns like: PushID(str), Button("") where they both have same id)
|
|
if query.Step < #query.Results then return "" end -- Only start using fallback below when all queries are done, so during queries we don't flickering ??? markers.
|
|
return "???"
|
|
end
|
|
|
|
local function GetResultAsPath(g, query, hex_encode_non_ascii_chars)
|
|
local out = {}
|
|
for n = 0, #query.Results - 1 do
|
|
local desc = FormatLevelInfo(g, query, n, false)
|
|
out[#out + 1] = (n == 0) and "//" or "/"
|
|
desc = string.gsub(desc, "/", "\\/")
|
|
if hex_encode_non_ascii_chars then
|
|
desc = string.gsub(desc, "[\128-\255]", function(c) return string.format("\\x%02x", string.byte(c)) end)
|
|
end
|
|
out[#out + 1] = desc
|
|
end
|
|
return table.concat(out)
|
|
end
|
|
|
|
-- ID Stack Tool: Display UI
|
|
function ImGui.ShowIDStackToolWindow(p_open)
|
|
local g = ImGui.GetCurrentContext()
|
|
if bit32.band(g.NextWindowData.HasFlags, ImGuiNextWindowDataFlags.HasSize) == 0 then
|
|
ImGui.SetNextWindowSize(ImVec2(0.0, ImGui.GetFontSize() * 8.0), ImGuiCond.FirstUseEver)
|
|
end
|
|
local visible
|
|
p_open, visible = ImGui.Begin("Dear ImGui ID Stack Tool", p_open)
|
|
if not visible or ImGui.GetCurrentWindow().BeginCount > 1 then
|
|
ImGui.End()
|
|
return p_open
|
|
end
|
|
local query = PathQuery(g)
|
|
local tool = g.DebugIDStackTool
|
|
tool.LastActiveFrame = g.FrameCount
|
|
local result_path = GetResultAsPath(g, query, tool.OptHexEncodeNonAsciiChars)
|
|
ImGui.Text("0x%08X", query.MainID)
|
|
ImGui.SameLine()
|
|
MetricsHelpMarker("Hover an item with the mouse to display elements of the ID Stack leading to the item's final ID.\nEach level of the stack correspond to a PushID() call.\nAll levels of the stack are hashed together to make the final ID of a widget (ID displayed at the bottom level of the stack).\nRead FAQ entry about the ID stack for details.")
|
|
|
|
-- Ctrl+C to copy path
|
|
local time_since_copy = g.Time - tool.CopyToClipboardLastTime
|
|
ImGui.PushStyleVarY(ImGuiStyleVar.FramePadding, 0.0)
|
|
_, tool.OptHexEncodeNonAsciiChars = ImGui.Checkbox("Hex-encode non-ASCII", tool.OptHexEncodeNonAsciiChars)
|
|
ImGui.SameLine()
|
|
_, tool.OptCopyToClipboardOnCtrlC = ImGui.Checkbox("Ctrl+C: copy path", tool.OptCopyToClipboardOnCtrlC)
|
|
ImGui.PopStyleVar()
|
|
ImGui.SameLine()
|
|
ImGui.TextColored((time_since_copy >= 0.0 and time_since_copy < 0.75 and math.fmod(time_since_copy, 0.25) < 0.25 * 0.5) and ImVec4(1.0, 1.0, 0.3, 1.0) or ImVec4(), "*COPIED*")
|
|
if tool.OptCopyToClipboardOnCtrlC and ImGui.Shortcut(bit32.bor(ImGuiMod_Ctrl, ImGuiKey.C), bit32.bor(ImGuiInputFlags.RouteGlobal, ImGuiInputFlags.RouteOverFocused)) then
|
|
tool.CopyToClipboardLastTime = g.Time
|
|
ImGui.SetClipboardText(result_path)
|
|
end
|
|
ImGui.Text("- Path \"%s\"", query.Complete and result_path or "")
|
|
ImGui.Separator()
|
|
|
|
-- Display decorated stack
|
|
if #query.Results > 0 and ImGui.BeginTable("##table", 3, ImGuiTableFlags.Borders) then
|
|
local id_width = ImGui.CalcTextSize("0xDDDDDDDD").x
|
|
ImGui.TableSetupColumn("Seed", ImGuiTableColumnFlags.WidthFixed, id_width)
|
|
ImGui.TableSetupColumn("PushID", ImGuiTableColumnFlags.WidthStretch)
|
|
ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, id_width)
|
|
ImGui.TableHeadersRow()
|
|
for n = 0, #query.Results - 1 do
|
|
local info = query.Results[n + 1]
|
|
ImGui.TableNextColumn()
|
|
ImGui.Text("0x%08X", (n > 0) and query.Results[n].ID or 0)
|
|
ImGui.TableNextColumn()
|
|
ImGui.TextUnformatted(FormatLevelInfo(g, query, n, true))
|
|
ImGui.TableNextColumn()
|
|
ImGui.Text("0x%08X", info.ID)
|
|
if n == #query.Results - 1 then ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, ImGui.GetColorU32(ImGuiCol.Header)) end
|
|
end
|
|
ImGui.EndTable()
|
|
end
|
|
ImGui.End()
|
|
return p_open
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- [SECTION] METRICS/DEBUGGER WINDOW
|
|
-------------------------------------------------------------------------------
|
|
-- - DebugTextEncoding()
|
|
-- - ShowMetricsWindow()
|
|
-- - DebugNodeColumns() [Internal]
|
|
-- - DebugNodeDockNode() [Internal]
|
|
-- - DebugNodeDrawList() [Internal]
|
|
-- - DebugNodeStorage() [Internal]
|
|
-- - DebugNodeTabBar() [Internal]
|
|
-- - DebugNodeTable() [Internal]
|
|
-- - DebugNodeTableSettings() [Internal]
|
|
-- - DebugNodeViewport() [Internal]
|
|
-- - DebugNodeWindow() [Internal]
|
|
-- - DebugNodeWindowSettings() [Internal]
|
|
-- - DebugNodeWindowsList() [Internal]
|
|
-- - DebugNodeWindowsListByBeginStackParent() [Internal]
|
|
-------------------------------------------------------------------------------
|
|
|
|
local function B(v) return v and 1 or 0 end
|
|
local function FG(window) return ImGui.GetForegroundDrawList(window and window.Viewport or ImGui.GetMainViewport()) end
|
|
local function YellowRect(window, min, max) FG(window):AddRect(min, max, IM_COL32(255, 255, 0, 255)) end
|
|
|
|
-- [DEBUG] Display contents of Columns
|
|
function ImGui.DebugNodeColumns(columns)
|
|
if not ImGui.TreeNode(columns.ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns.ID, columns.Count, columns.Flags) then return end
|
|
ImGui.BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns.OffMaxX - columns.OffMinX, columns.OffMinX, columns.OffMaxX)
|
|
for i, column in columns.Columns:iter() do
|
|
ImGui.BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", i - 1, column.OffsetNorm, ImGui.GetColumnOffsetFromNorm(columns, column.OffsetNorm))
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- [DEBUG] Display contents of ImGuiStorage
|
|
function ImGui.DebugNodeStorage(storage, label)
|
|
local n = 0
|
|
for _ in pairs(storage) do n = n + 1 end
|
|
if not ImGui.TreeNode(label, "%s: %d entries", label, n) then return end
|
|
for k, v in pairs(storage) do
|
|
ImGui.BulletText("Key 0x%08X Value { %s }", tonumber(k) or 0, tostring(v))
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- [DEBUG] Display contents of ImDrawList
|
|
-- Note that both 'window' and 'viewport' may be nil here. Viewport is generally nil of destroyed popups which previously owned a viewport.
|
|
function ImGui.DebugNodeDrawList(window, viewport, draw_list, label)
|
|
local g = ImGui.GetCurrentContext()
|
|
local cfg = g.DebugMetricsConfig
|
|
local cmd_count = draw_list.CmdBuffer.Size
|
|
if cmd_count > 0 and draw_list.CmdBuffer.Data[cmd_count].ElemCount == 0 and draw_list.CmdBuffer.Data[cmd_count].UserCallback == nil then
|
|
cmd_count = cmd_count - 1
|
|
end
|
|
local node_open = ImGui.TreeNode(tostring(draw_list), "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list._OwnerName or "", draw_list.VtxBuffer.Size, draw_list.IdxBuffer.Size, cmd_count)
|
|
if draw_list == ImGui.GetWindowDrawList() then
|
|
ImGui.SameLine()
|
|
ImGui.TextColored(ImVec4(1.0, 0.4, 0.4, 1.0), "CURRENTLY APPENDING") -- Can't display stats for active draw list! (we don't have the data double-buffered)
|
|
if node_open then ImGui.TreePop() end
|
|
return
|
|
end
|
|
local fg_draw_list = viewport and ImGui.GetForegroundDrawList(viewport) or nil -- Render additional visuals into the top-most draw list
|
|
if window and ImGui.IsItemHovered() and fg_draw_list then
|
|
fg_draw_list:AddRect(window.Pos, window.Pos + window.Size, IM_COL32(255, 255, 0, 255))
|
|
end
|
|
if not node_open then return end
|
|
if window and not window.WasActive then ImGui.TextDisabled("Warning: owning Window is inactive. This DrawList is not being rendered!") end
|
|
|
|
local vtx, idx = draw_list.VtxBuffer.Buf, draw_list.IdxBuffer.Data
|
|
for cmd_n = 1, cmd_count do
|
|
local pcmd = draw_list.CmdBuffer.Data[cmd_n]
|
|
if pcmd.UserCallback then
|
|
ImGui.BulletText("Callback %s", tostring(pcmd.UserCallback))
|
|
else
|
|
local cr = pcmd.ClipRect
|
|
local open = ImGui.TreeNode(cmd_n, "DrawCmd:%5d tris, Tex %s, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", math.floor(pcmd.ElemCount / 3), tostring(pcmd.TexRef and (pcmd.TexRef._TexID or pcmd.TexRef._TexData)), cr.x, cr.y, cr.z, cr.w)
|
|
local function ShowMesh(show_mesh, show_aabb)
|
|
if not fg_draw_list then return end
|
|
local clip_rect = ImRect(cr.x, cr.y, cr.z, cr.w)
|
|
local vtxs_rect = ImRect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX)
|
|
local base = pcmd.IdxOffset
|
|
for i = base, base + pcmd.ElemCount - 1, 3 do
|
|
local tri = {}
|
|
for n = 0, 2 do
|
|
local vi = idx[i + n + 1] + pcmd.VtxOffset
|
|
if vi > draw_list.VtxBuffer.Size then return end
|
|
tri[n + 1] = ImVec2(ImDrawVtx_Pos(vtx, vi))
|
|
vtxs_rect:Add(tri[n + 1])
|
|
end
|
|
if show_mesh then fg_draw_list:AddPolyline(tri, 3, IM_COL32(255, 255, 0, 255), 1.0, ImDrawFlags.Closed) end -- In yellow: mesh triangles
|
|
end
|
|
if show_aabb then
|
|
fg_draw_list:AddRect(ImTrunc(clip_rect.Min), ImTrunc(clip_rect.Max), IM_COL32(255, 0, 255, 255)) -- In pink: clipping rectangle submitted to GPU
|
|
fg_draw_list:AddRect(ImTrunc(vtxs_rect.Min), ImTrunc(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)) -- In cyan: bounding box of triangles
|
|
end
|
|
end
|
|
if ImGui.IsItemHovered() and (cfg.ShowDrawCmdMesh or cfg.ShowDrawCmdBoundingBoxes) then ShowMesh(cfg.ShowDrawCmdMesh, cfg.ShowDrawCmdBoundingBoxes) end
|
|
if open then
|
|
ImGui.Selectable(string.format("Mesh: ElemCount: %d, VtxOffset: +%d, IdxOffset: +%d", pcmd.ElemCount, pcmd.VtxOffset, pcmd.IdxOffset))
|
|
if ImGui.IsItemHovered() then ShowMesh(true, false) end
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- [DEBUG] Display contents of ImGuiTabBar
|
|
function ImGui.DebugNodeTabBar(tab_bar, label)
|
|
-- Standalone tab bars (not associated to docking/windows functionality) currently hold no discernible strings.
|
|
local g = ImGui.GetCurrentContext()
|
|
local is_active = (tab_bar.PrevFrameVisible >= ImGui.GetFrameCount() - 2)
|
|
local names = {}
|
|
for tab_n = 1, math.min(tab_bar.Tabs.Size, 3) do
|
|
local tab = tab_bar.Tabs.Data[tab_n]
|
|
names[#names + 1] = "'" .. (ImGui.TabBarGetTabName and ImGui.TabBarGetTabName(tab_bar, tab) or "?") .. "'"
|
|
end
|
|
local buf = string.format("%s 0x%08X (%d tabs)%s { %s%s", label, tab_bar.ID, tab_bar.Tabs.Size, is_active and "" or " *Inactive*", table.concat(names, ", "), (tab_bar.Tabs.Size > 3) and " ... }" or " } ")
|
|
if not is_active then ImGui.PushStyleColor(ImGuiCol.Text, g.Style.Colors[ImGuiCol.TextDisabled]) end
|
|
local open = ImGui.TreeNode(label, "%s", buf)
|
|
if not is_active then ImGui.PopStyleColor() end
|
|
if is_active and ImGui.IsItemHovered() then
|
|
local dl = FG(tab_bar.Window)
|
|
dl:AddRect(tab_bar.BarRect.Min, tab_bar.BarRect.Max, IM_COL32(255, 255, 0, 255))
|
|
dl:AddLine(ImVec2(tab_bar.ScrollingRectMinX, tab_bar.BarRect.Min.y), ImVec2(tab_bar.ScrollingRectMinX, tab_bar.BarRect.Max.y), IM_COL32(0, 255, 0, 255))
|
|
dl:AddLine(ImVec2(tab_bar.ScrollingRectMaxX, tab_bar.BarRect.Min.y), ImVec2(tab_bar.ScrollingRectMaxX, tab_bar.BarRect.Max.y), IM_COL32(0, 255, 0, 255))
|
|
end
|
|
if open then
|
|
for tab_n, tab in tab_bar.Tabs:iter() do
|
|
ImGui.PushID(tab_n)
|
|
if ImGui.SmallButton("<") and ImGui.TabBarQueueReorder then ImGui.TabBarQueueReorder(tab_bar, tab, -1) end ImGui.SameLine(0, 2)
|
|
if ImGui.SmallButton(">") and ImGui.TabBarQueueReorder then ImGui.TabBarQueueReorder(tab_bar, tab, 1) end ImGui.SameLine()
|
|
ImGui.Text("%02d%s Tab 0x%08X '%s' Offset: %.2f, Width: %.2f/%.2f", tab_n - 1, (tab.ID == tab_bar.SelectedTabId) and "*" or " ", tab.ID,
|
|
ImGui.TabBarGetTabName and ImGui.TabBarGetTabName(tab_bar, tab) or "?", tab.Offset, tab.Width, tab.ContentWidth)
|
|
ImGui.PopID()
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
|
|
local function SizingDesc(f)
|
|
local TF = ImGuiTableFlags
|
|
f = bit32.band(f, TF.SizingMask_)
|
|
if f == TF.SizingFixedFit then return "FixedFit" elseif f == TF.SizingFixedSame then return "FixedSame"
|
|
elseif f == TF.SizingStretchProp then return "StretchProp" elseif f == TF.SizingStretchSame then return "StretchSame" end
|
|
return "N/A"
|
|
end
|
|
|
|
function ImGui.DebugNodeTableSettings(settings, tbl)
|
|
local open = ImGui.TreeNode(tostring(settings), "Settings 0x%08X (%d columns)", settings.ID, settings.ColumnsCount)
|
|
local hovered = ImGui.IsItemHovered()
|
|
if hovered and tbl == nil and settings.ID ~= 0 then tbl = ImGui.TableFindByID(settings.ID) end
|
|
if hovered and tbl ~= nil then YellowRect(tbl.OuterWindow, tbl.OuterRect.Min, tbl.OuterRect.Max) end
|
|
if open then
|
|
ImGui.BulletText("SaveFlags: 0x%08X", settings.SaveFlags)
|
|
ImGui.BulletText("ColumnsCount: %d (max %d)", settings.ColumnsCount, settings.ColumnsCountMax)
|
|
for n = 0, settings.ColumnsCount - 1 do
|
|
local c = settings.Columns[n]
|
|
local dir = (c.SortOrder ~= -1) and c.SortDirection or ImGuiSortDirection.None
|
|
ImGui.BulletText("Column %d Order %d SortOrder %2d %s Vis %d %s %7.3f ID 0x%08X", n, c.DisplayOrder, c.SortOrder,
|
|
(dir == ImGuiSortDirection.Ascending) and "Asc" or ((dir == ImGuiSortDirection.Descending) and "Des" or "---"), c.IsEnabled, (c.IsStretch ~= 0) and "Weight" or "Width ", c.WidthOrWeight, c.ID)
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
|
|
function ImGui.DebugNodeTable(tbl)
|
|
local g = ImGui.GetCurrentContext()
|
|
local is_active = (tbl.LastFrameActive >= g.FrameCount - 2) -- Note that fully clipped early out scrolling tables will appear as inactive here.
|
|
if not is_active then ImGui.PushStyleColor(ImGuiCol.Text, g.Style.Colors[ImGuiCol.TextDisabled]) end
|
|
local open = ImGui.TreeNode(tostring(tbl), "Table 0x%08X (%d columns, in '%s')%s", tbl.ID, tbl.ColumnsCount, tbl.OuterWindow and tbl.OuterWindow.Name or "?", is_active and "" or " *Inactive*")
|
|
if not is_active then ImGui.PopStyleColor() end
|
|
if ImGui.IsItemHovered() then YellowRect(tbl.OuterWindow, tbl.OuterRect.Min, tbl.OuterRect.Max) end
|
|
if not open then return end
|
|
if tbl.InstanceCurrent > 0 then ImGui.Text("** %d instances of same table! Some data below will refer to last instance.", tbl.InstanceCurrent + 1) end
|
|
local clear_settings = ImGui.SmallButton("Clear settings")
|
|
ImGui.BulletText("OuterRect: Pos: (%.1f,%.1f) Size: (%.1f,%.1f) Sizing: '%s'", tbl.OuterRect.Min.x, tbl.OuterRect.Min.y, tbl.OuterRect:GetWidth(), tbl.OuterRect:GetHeight(), SizingDesc(tbl.Flags))
|
|
ImGui.BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", tbl.ColumnsGivenWidth, tbl.ColumnsAutoFitWidth, tbl.InnerWidth, (tbl.InnerWidth == 0.0) and " (auto)" or "")
|
|
ImGui.BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", tbl.CellPaddingX, tbl.CellSpacingX1, tbl.CellSpacingX2, tbl.OuterPaddingX)
|
|
ImGui.BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", tbl.HoveredColumnBody, tbl.HoveredColumnBorder)
|
|
ImGui.BulletText("ResizedColumn: %d, HeldHeaderColumn: %d, ReorderColumn: %d", tbl.LastResizedColumn, tbl.LastHeldHeaderColumn, tbl.ReorderColumn)
|
|
for n = 0, tbl.InstanceCurrent do
|
|
local inst = ImGui.TableGetInstanceData(tbl, n)
|
|
ImGui.BulletText("Instance %d: HoveredRow: %d, LastOuterHeight: %.2f", n, inst.HoveredRowLast, inst.LastOuterHeight)
|
|
end
|
|
local sum_weights = 0.0
|
|
for n = 0, tbl.ColumnsCount - 1 do
|
|
if bit32.band(tbl.Columns[n].Flags, ImGuiTableColumnFlags.WidthStretch) ~= 0 then sum_weights = sum_weights + tbl.Columns[n].StretchWeight end
|
|
end
|
|
for n = 0, tbl.ColumnsCount - 1 do
|
|
local c = tbl.Columns[n]
|
|
local CF = ImGuiTableColumnFlags
|
|
local buf = string.format(
|
|
"Column %d order %d '%s': offset %+.2f to %+.2f%s\nEnabled: %d, VisibleX/Y: %d/%d, RequestOutput: %d, SkipItems: %d, DrawChannels: %d,%d\n" ..
|
|
"WidthGiven: %.1f, Request/Auto: %.1f/%.1f, StretchWeight: %.3f (%.1f%%)\nMinX: %.1f, MaxX: %.1f (%+.1f), ClipRect: %.1f to %.1f (+%.1f)\n" ..
|
|
"ContentWidth: %.1f,%.1f, HeadersUsed/Ideal %.1f/%.1f\nSort: %d%s, UserData: 0x%08X, Flags: 0x%04X: %s%s%s..",
|
|
n, c.DisplayOrder, ImGui.TableGetColumnName(tbl, n) or "", c.MinX - tbl.WorkRect.Min.x, c.MaxX - tbl.WorkRect.Min.x, (n < tbl.FreezeColumnsRequest) and " (Frozen)" or "",
|
|
B(c.IsEnabled), B(c.IsVisibleX), B(c.IsVisibleY), B(c.IsRequestOutput), B(c.IsSkipItems), c.DrawChannelFrozen, c.DrawChannelUnfrozen,
|
|
c.WidthGiven, c.WidthRequest, c.WidthAuto, c.StretchWeight, (c.StretchWeight > 0.0 and sum_weights > 0) and (c.StretchWeight / sum_weights) * 100.0 or 0.0,
|
|
c.MinX, c.MaxX, c.MaxX - c.MinX, c.ClipRect.Min.x, c.ClipRect.Max.x, c.ClipRect.Max.x - c.ClipRect.Min.x,
|
|
c.ContentMaxXFrozen - c.WorkMinX, c.ContentMaxXUnfrozen - c.WorkMinX, c.ContentMaxXHeadersUsed - c.WorkMinX, c.ContentMaxXHeadersIdeal - c.WorkMinX,
|
|
c.SortOrder, (c.SortDirection == ImGuiSortDirection.Ascending) and " (Asc)" or ((c.SortDirection == ImGuiSortDirection.Descending) and " (Des)" or ""), tonumber(c.UserData) or 0, c.Flags,
|
|
(bit32.band(c.Flags, CF.WidthStretch) ~= 0) and "WidthStretch " or "", (bit32.band(c.Flags, CF.WidthFixed) ~= 0) and "WidthFixed " or "", (bit32.band(c.Flags, CF.NoResize) ~= 0) and "NoResize " or "")
|
|
ImGui.Bullet()
|
|
ImGui.Selectable(buf)
|
|
if ImGui.IsItemHovered() then YellowRect(tbl.OuterWindow, ImVec2(c.MinX, tbl.OuterRect.Min.y), ImVec2(c.MaxX, tbl.OuterRect.Max.y)) end
|
|
end
|
|
local settings = ImGui.TableGetBoundSettings(tbl)
|
|
if settings then ImGui.DebugNodeTableSettings(settings, tbl) end
|
|
if clear_settings then tbl.IsResetAllRequest = true end -- Queue a call to TableResetSettings()
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
local function DebugNodeDockNodeFlags(flags, label, enabled)
|
|
local D = ImGuiDockNodeFlags
|
|
ImGui.PushID(label)
|
|
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, ImVec2(0.0, 0.0))
|
|
ImGui.Text("%s:", label)
|
|
if not enabled then ImGui.BeginDisabled() end
|
|
for _, n in ipairs({ "NoResize", "NoResizeX", "NoResizeY", "NoTabBar", "HiddenTabBar", "NoWindowMenuButton", "NoCloseButton", "DockedWindowsInFocusRoute",
|
|
"NoDocking", "NoDockingSplit", "NoDockingSplitOther", "NoDockingOverMe", "NoDockingOverOther", "NoDockingOverEmpty", "NoUndocking" }) do
|
|
if D[n] then _, flags = ImGui.CheckboxFlags(n, flags, D[n]) end
|
|
end
|
|
if not enabled then ImGui.EndDisabled() end
|
|
ImGui.PopStyleVar()
|
|
ImGui.PopID()
|
|
return flags
|
|
end
|
|
|
|
-- [DEBUG] Display contents of ImDockNode
|
|
function ImGui.DebugNodeDockNode(node, label)
|
|
local g = ImGui.GetCurrentContext()
|
|
local is_alive = (g.FrameCount - node.LastFrameAlive < 2) -- Submitted with ImGuiDockNodeFlags.KeepAliveOnly
|
|
local is_active = (g.FrameCount - node.LastFrameActive < 2) -- Submitted
|
|
if not is_alive then ImGui.PushStyleColor(ImGuiCol.Text, g.Style.Colors[ImGuiCol.TextDisabled]) end
|
|
local tree_node_flags = node.IsFocused and ImGuiTreeNodeFlags.Selected or ImGuiTreeNodeFlags.None
|
|
local open
|
|
if node.Windows.Size > 0 then
|
|
open = ImGui.TreeNodeEx(node.ID, tree_node_flags, "%s 0x%04X%s: %d windows (vis: '%s')", label, node.ID, node.IsVisible and "" or " (hidden)", node.Windows.Size, node.VisibleWindow and node.VisibleWindow.Name or "NULL")
|
|
else
|
|
local kind = (node.SplitAxis == ImGuiAxis.X) and "horizontal split" or ((node.SplitAxis == ImGuiAxis.Y) and "vertical split" or "empty")
|
|
open = ImGui.TreeNodeEx(node.ID, tree_node_flags, "%s 0x%04X%s: %s (vis: '%s')", label, node.ID, node.IsVisible and "" or " (hidden)", kind, node.VisibleWindow and node.VisibleWindow.Name or "NULL")
|
|
end
|
|
if not is_alive then ImGui.PopStyleColor() end
|
|
if is_active and ImGui.IsItemHovered() then
|
|
local window = node.HostWindow or node.VisibleWindow
|
|
if window then YellowRect(window, node.Pos, node.Pos + node.Size) end
|
|
end
|
|
if open then
|
|
ImGui.BulletText("Pos (%.0f,%.0f), Size (%.0f, %.0f) Ref (%.0f, %.0f)", node.Pos.x, node.Pos.y, node.Size.x, node.Size.y, node.SizeRef.x, node.SizeRef.y)
|
|
ImGui.DebugNodeWindow(node.HostWindow, "HostWindow")
|
|
ImGui.DebugNodeWindow(node.VisibleWindow, "VisibleWindow")
|
|
ImGui.BulletText("SelectedTabID: 0x%08X, LastFocusedNodeID: 0x%08X", node.SelectedTabId, node.LastFocusedNodeId)
|
|
ImGui.BulletText("Misc:%s%s%s%s%s%s%s", node:IsDockSpace() and " IsDockSpace" or "", node:IsCentralNode() and " IsCentralNode" or "",
|
|
is_alive and " IsAlive" or "", is_active and " IsActive" or "", node.IsFocused and " IsFocused" or "", node.WantLockSizeOnce and " WantLockSizeOnce" or "", node.HasCentralNodeChild and " HasCentralNodeChild" or "")
|
|
if ImGui.TreeNode("flags", "Flags Merged: 0x%04X, Local: 0x%04X, InWindows: 0x%04X, Shared: 0x%04X", node.MergedFlags, node.LocalFlags, node.LocalFlagsInWindows, node.SharedFlags) then
|
|
if ImGui.BeginTable("flags", 4) then
|
|
ImGui.TableNextColumn(); DebugNodeDockNodeFlags(node.MergedFlags, "MergedFlags", false)
|
|
ImGui.TableNextColumn(); node.LocalFlags = DebugNodeDockNodeFlags(node.LocalFlags, "LocalFlags", true)
|
|
ImGui.TableNextColumn(); DebugNodeDockNodeFlags(node.LocalFlagsInWindows, "LocalFlagsInWindows", false)
|
|
ImGui.TableNextColumn(); node.SharedFlags = DebugNodeDockNodeFlags(node.SharedFlags, "SharedFlags", true)
|
|
ImGui.EndTable()
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
if node.ParentNode then ImGui.DebugNodeDockNode(node.ParentNode, "ParentNode") end
|
|
if node.ChildNodes[0] then ImGui.DebugNodeDockNode(node.ChildNodes[0], "Child[0]") end
|
|
if node.ChildNodes[1] then ImGui.DebugNodeDockNode(node.ChildNodes[1], "Child[1]") end
|
|
if node.TabBar then ImGui.DebugNodeTabBar(node.TabBar, "TabBar") end
|
|
ImGui.DebugNodeWindowsList(node.Windows, "Windows")
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
|
|
function ImGui.DebugNodeWindow(window, label)
|
|
if window == nil then
|
|
ImGui.BulletText("%s: NULL", label)
|
|
return
|
|
end
|
|
local g = ImGui.GetCurrentContext()
|
|
local is_active = window.WasActive
|
|
local tree_node_flags = (window == g.NavWindow) and ImGuiTreeNodeFlags.Selected or ImGuiTreeNodeFlags.None
|
|
if not is_active then ImGui.PushStyleColor(ImGuiCol.Text, g.Style.Colors[ImGuiCol.TextDisabled]) end
|
|
local open = ImGui.TreeNodeEx(label, tree_node_flags, "%s '%s'%s", label, window.Name, is_active and "" or " *Inactive*")
|
|
if not is_active then ImGui.PopStyleColor() end
|
|
if ImGui.IsItemHovered() and is_active then YellowRect(window, window.Pos, window.Pos + window.Size) end
|
|
if not open then return end
|
|
if window.MemoryCompacted then ImGui.TextDisabled("Note: some memory buffers have been compacted/freed.") end
|
|
local W = ImGuiWindowFlags
|
|
local flags = window.Flags
|
|
local function F(f, s) return (bit32.band(flags, f) ~= 0) and s or "" end
|
|
ImGui.DebugNodeDrawList(window, window.Viewport, window.DrawList, "DrawList")
|
|
ImGui.BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f) Ideal (%.1f,%.1f)", window.Pos.x, window.Pos.y, window.Size.x, window.Size.y, window.ContentSize.x, window.ContentSize.y, window.ContentSizeIdeal.x, window.ContentSizeIdeal.y)
|
|
ImGui.BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, F(W.ChildWindow, "Child "), F(W.Tooltip, "Tooltip "), F(W.Popup, "Popup "), F(W.Modal, "Modal "), F(W.ChildMenu, "ChildMenu "),
|
|
F(W.NoSavedSettings, "NoSavedSettings "), F(W.NoMouseInputs, "NoMouseInputs"), F(W.NoNavInputs, "NoNavInputs"), F(W.AlwaysAutoResize, "AlwaysAutoResize"))
|
|
if bit32.band(flags, W.ChildWindow) ~= 0 then
|
|
local cf = window.ChildFlags or 0
|
|
local C = ImGuiChildFlags
|
|
ImGui.BulletText("ChildFlags: 0x%08X (%s%s%s%s..)", cf, (bit32.band(cf, C.Borders) ~= 0) and "Borders " or "", (bit32.band(cf, C.ResizeX) ~= 0) and "ResizeX " or "",
|
|
(bit32.band(cf, C.ResizeY) ~= 0) and "ResizeY " or "", (bit32.band(cf, C.NavFlattened) ~= 0) and "NavFlattened " or "")
|
|
end
|
|
ImGui.BulletText("WindowClassId: 0x%08X", window.WindowClass and window.WindowClass.ClassId or 0)
|
|
ImGui.BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f) Scrollbar:%s%s", window.Scroll.x, window.ScrollMax.x, window.Scroll.y, window.ScrollMax.y, window.ScrollbarX and "X" or "", window.ScrollbarY and "Y" or "")
|
|
ImGui.BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", B(window.Active), B(window.WasActive), B(window.WriteAccessed), (window.Active or window.WasActive) and window.BeginOrderWithinContext or -1)
|
|
ImGui.BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", B(window.Appearing), B(window.Hidden), window.HiddenFramesCanSkipItems, window.HiddenFramesCannotSkipItems, B(window.SkipItems))
|
|
for layer = 0, 1 do
|
|
local r = window.NavRectRel[layer]
|
|
if r.Min.x >= r.Max.x and r.Min.y >= r.Max.y then
|
|
ImGui.BulletText("NavLastIds[%d]: 0x%08X", layer, window.NavLastIds[layer])
|
|
else
|
|
ImGui.BulletText("NavLastIds[%d]: 0x%08X at +(%.1f,%.1f)(%.1f,%.1f)", layer, window.NavLastIds[layer], r.Min.x, r.Min.y, r.Max.x, r.Max.y)
|
|
end
|
|
end
|
|
ImGui.BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window.DC.NavLayersActiveMask, window.NavLastChildNavWindow and window.NavLastChildNavWindow.Name or "NULL")
|
|
ImGui.BulletText("Viewport: %d%s, ViewportId: 0x%08X, ViewportPos: (%.1f,%.1f)", window.Viewport and (window.Viewport.Idx or 0) or -1, window.ViewportOwned and " (Owned)" or "", window.ViewportId or 0, window.ViewportPos.x, window.ViewportPos.y)
|
|
ImGui.BulletText("DockId: 0x%04X, DockOrder: %d, Act: %d, Vis: %d", window.DockId or 0, window.DockOrder or -1, B(window.DockIsActive), B(window.DockTabIsVisible))
|
|
if window.DockNode or window.DockNodeAsHost then
|
|
ImGui.DebugNodeDockNode(window.DockNodeAsHost or window.DockNode, window.DockNodeAsHost and "DockNodeAsHost" or "DockNode")
|
|
end
|
|
if window.RootWindow ~= window then ImGui.DebugNodeWindow(window.RootWindow, "RootWindow") end
|
|
if window.RootWindowDockTree and window.RootWindowDockTree ~= window.RootWindow then ImGui.DebugNodeWindow(window.RootWindowDockTree, "RootWindowDockTree") end
|
|
if window.ParentWindow ~= nil then ImGui.DebugNodeWindow(window.ParentWindow, "ParentWindow") end
|
|
if window.ParentWindowForFocusRoute ~= nil then ImGui.DebugNodeWindow(window.ParentWindowForFocusRoute, "ParentWindowForFocusRoute") end
|
|
if window.DC.ChildWindows.Size > 0 then ImGui.DebugNodeWindowsList(window.DC.ChildWindows, "ChildWindows") end
|
|
if window.ColumnsStorage.Size > 0 and ImGui.TreeNode("Columns", "Columns sets (%d)", window.ColumnsStorage.Size) then
|
|
for _, columns in window.ColumnsStorage:iter() do ImGui.DebugNodeColumns(columns) end
|
|
ImGui.TreePop()
|
|
end
|
|
ImGui.DebugNodeStorage(window.StateStorage, "Storage")
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
function ImGui.DebugNodeWindowSettings(settings)
|
|
if settings.WantDelete then ImGui.BeginDisabled() end
|
|
ImGui.BulletText("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", settings.ID, settings.Name or "", settings.Pos.x, settings.Pos.y, settings.Size.x, settings.Size.y, B(settings.Collapsed))
|
|
if settings.WantDelete then ImGui.EndDisabled() end
|
|
end
|
|
|
|
function ImGui.DebugNodeWindowsList(windows, label)
|
|
if not ImGui.TreeNode(label, "%s (%d)", label, windows.Size) then return end
|
|
for i = windows.Size, 1, -1 do -- Iterate front to back
|
|
ImGui.PushID(tostring(windows.Data[i]))
|
|
ImGui.DebugNodeWindow(windows.Data[i], "Window")
|
|
ImGui.PopID()
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- FIXME-OPT: This is technically suboptimal, but it is simpler this way.
|
|
function ImGui.DebugNodeWindowsListByBeginStackParent(windows, first, last, parent_in_begin_stack)
|
|
for i = first, last do
|
|
local window = windows[i]
|
|
if window.ParentWindowInBeginStack == parent_in_begin_stack then
|
|
local buf = string.format("[%04d] Window", window.BeginOrderWithinContext)
|
|
-- ImGui.BulletText("[%04d] Window '%s'", window.BeginOrderWithinContext, window.Name)
|
|
ImGui.DebugNodeWindow(window, buf)
|
|
ImGui.TreePush(buf)
|
|
ImGui.DebugNodeWindowsListByBeginStackParent(windows, i + 1, last, window)
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
end
|
|
|
|
function ImGui.DebugNodeViewport(viewport)
|
|
local g = ImGui.GetCurrentContext()
|
|
ImGui.SetNextItemOpen(true, ImGuiCond.Once)
|
|
local open = ImGui.TreeNode(viewport.ID, "Viewport #%d, ID: 0x%08X, Parent: 0x%08X, Window: \"%s\"", viewport.Idx or 0, viewport.ID, viewport.ParentViewportId or 0, viewport.Window and viewport.Window.Name or "N/A")
|
|
if ImGui.IsItemHovered() then g.DebugMetricsConfig.HighlightViewportID = viewport.ID end
|
|
if open then
|
|
ImGui.BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nFrameBufferScale: (%.2f,%.2f)\nWorkArea Inset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f\nMonitor: %d, DpiScale: %.0f%%",
|
|
viewport.Pos.x, viewport.Pos.y, viewport.Size.x, viewport.Size.y, viewport.FramebufferScale and viewport.FramebufferScale.x or 1, viewport.FramebufferScale and viewport.FramebufferScale.y or 1,
|
|
viewport.WorkInsetMin.x, viewport.WorkInsetMin.y, viewport.WorkInsetMax.x, viewport.WorkInsetMax.y, viewport.PlatformMonitor or 0, (viewport.DpiScale or 1) * 100.0)
|
|
ImGui.BulletText("Flags: 0x%04X", viewport.Flags or 0)
|
|
if viewport.DrawDataP then
|
|
for _, draw_list in viewport.DrawDataP.CmdLists:iter() do ImGui.DebugNodeDrawList(nil, viewport, draw_list, "DrawList") end
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
|
|
-- Helper tool to diagnose between text encoding issues and font loading issues. Pass your UTF-8 string and verify that there are correct.
|
|
function ImGui.DebugTextEncoding(str)
|
|
ImGui.Text("Text: \"%s\"", str)
|
|
if not ImGui.BeginTable("##DebugTextEncoding", 4, bit32.bor(ImGuiTableFlags.Borders, ImGuiTableFlags.RowBg, ImGuiTableFlags.SizingFixedFit, ImGuiTableFlags.Resizable)) then return end
|
|
ImGui.TableSetupColumn("Offset")
|
|
ImGui.TableSetupColumn("UTF-8")
|
|
ImGui.TableSetupColumn("Glyph")
|
|
ImGui.TableSetupColumn("Codepoint")
|
|
ImGui.TableHeadersRow()
|
|
for pos, c in utf8.codes(str) do
|
|
local nxt = utf8.offset(str, 2, pos) or (#str + 1)
|
|
ImGui.TableNextColumn()
|
|
ImGui.Text("%d", pos - 1)
|
|
ImGui.TableNextColumn()
|
|
for b = pos, nxt - 1 do
|
|
if b > pos then ImGui.SameLine() end
|
|
ImGui.Text("0x%02X", string.byte(str, b))
|
|
end
|
|
ImGui.TableNextColumn()
|
|
ImGui.TextUnformatted(string.sub(str, pos, nxt - 1))
|
|
ImGui.TableNextColumn()
|
|
ImGui.Text("U+%04X", c)
|
|
end
|
|
ImGui.EndTable()
|
|
end
|
|
|
|
-- Debugging enums
|
|
local WRT_Names = { "OuterRect", "OuterRectClipped", "InnerRect", "InnerClipRect", "WorkRect", "Content", "ContentIdeal", "ContentRegionRect" }
|
|
local TRT_Names = { "OuterRect", "InnerRect", "WorkRect", "HostClipRect", "InnerClipRect", "BackgroundClipRect", "ColumnsRect", "ColumnsWorkRect", "ColumnsClipRect",
|
|
"ColumnsContentHeadersUsed", "ColumnsContentHeadersIdeal", "ColumnsContentFrozen", "ColumnsContentUnfrozen" }
|
|
local TRT_ColumnsRect = 6
|
|
|
|
local function GetWindowRect(window, t)
|
|
if t == 0 then return window:Rect()
|
|
elseif t == 1 then return window.OuterRectClipped
|
|
elseif t == 2 then return window.InnerRect
|
|
elseif t == 3 then return window.InnerClipRect
|
|
elseif t == 4 then return window.WorkRect
|
|
elseif t == 5 then local m = window.InnerRect.Min - window.Scroll + window.WindowPadding; return ImRect(m, m + window.ContentSize)
|
|
elseif t == 6 then local m = window.InnerRect.Min - window.Scroll + window.WindowPadding; return ImRect(m, m + window.ContentSizeIdeal)
|
|
else return window.ContentRegionRect end
|
|
end
|
|
|
|
local function GetTableRect(tbl, t, n)
|
|
local inst = ImGui.TableGetInstanceData(tbl, tbl.InstanceCurrent) -- Always using last submitted instance
|
|
if t == 0 then return tbl.OuterRect elseif t == 1 then return tbl.InnerRect elseif t == 2 then return tbl.WorkRect
|
|
elseif t == 3 then return tbl.HostClipRect elseif t == 4 then return tbl.InnerClipRect elseif t == 5 then return tbl.BgClipRect end
|
|
local c = tbl.Columns[n]
|
|
if t == 6 then return ImRect(c.MinX, tbl.InnerClipRect.Min.y, c.MaxX, tbl.InnerClipRect.Min.y + inst.LastOuterHeight)
|
|
elseif t == 7 then return ImRect(c.WorkMinX, tbl.WorkRect.Min.y, c.WorkMaxX, tbl.WorkRect.Max.y)
|
|
elseif t == 8 then return c.ClipRect
|
|
elseif t == 9 then return ImRect(c.WorkMinX, tbl.InnerClipRect.Min.y, c.ContentMaxXHeadersUsed, tbl.InnerClipRect.Min.y + inst.LastTopHeadersRowHeight) -- Note: y1/y2 not always accurate
|
|
elseif t == 10 then return ImRect(c.WorkMinX, tbl.InnerClipRect.Min.y, c.ContentMaxXHeadersIdeal, tbl.InnerClipRect.Min.y + inst.LastTopHeadersRowHeight)
|
|
elseif t == 11 then return ImRect(c.WorkMinX, tbl.InnerClipRect.Min.y, c.ContentMaxXFrozen, tbl.InnerClipRect.Min.y + inst.LastFrozenHeight)
|
|
else return ImRect(c.WorkMinX, tbl.InnerClipRect.Min.y + inst.LastFrozenHeight, c.ContentMaxXUnfrozen, tbl.InnerClipRect.Max.y) end
|
|
end
|
|
|
|
local function ForEachTable(g, fn)
|
|
for i = 0, g.Tables:GetMapSize() - 1 do
|
|
local t = g.Tables:GetByIndex(i)
|
|
if t then fn(t) end
|
|
end
|
|
end
|
|
|
|
local MetricsEncodingBuf = nil
|
|
local MetricsDockRootOnly = true
|
|
function ImGui.ShowMetricsWindow(p_open)
|
|
local g = ImGui.GetCurrentContext()
|
|
local io = g.IO
|
|
local cfg = g.DebugMetricsConfig
|
|
if cfg.ShowDebugLog then cfg.ShowDebugLog = ImGui.ShowDebugLogWindow(cfg.ShowDebugLog) end
|
|
if cfg.ShowIDStackTool then cfg.ShowIDStackTool = ImGui.ShowIDStackToolWindow(cfg.ShowIDStackTool) end
|
|
|
|
local visible
|
|
p_open, visible = ImGui.Begin("Dear ImGui Metrics/Debugger", p_open)
|
|
if not visible or ImGui.GetCurrentWindow().BeginCount > 1 then
|
|
ImGui.End()
|
|
return p_open
|
|
end
|
|
|
|
-- Basic info
|
|
ImGui.Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM)
|
|
ImGui.Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0 / math.max(io.Framerate, 0.0001), io.Framerate)
|
|
ImGui.Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, math.floor(io.MetricsRenderIndices / 3))
|
|
ImGui.Text("%d visible windows, %d current allocations", io.MetricsRenderWindows, g.DebugAllocInfo.TotalAllocCount - g.DebugAllocInfo.TotalFreeCount)
|
|
ImGui.Separator()
|
|
|
|
if cfg.ShowWindowsRectsType == nil or cfg.ShowWindowsRectsType < 0 then cfg.ShowWindowsRectsType = 4 end
|
|
if cfg.ShowTablesRectsType == nil or cfg.ShowTablesRectsType < 0 then cfg.ShowTablesRectsType = 2 end
|
|
|
|
-- Tools
|
|
if ImGui.TreeNode("Tools") then
|
|
ImGui.SeparatorText("Visualize")
|
|
_, cfg.ShowDebugLog = ImGui.Checkbox("Show Debug Log", cfg.ShowDebugLog == true)
|
|
ImGui.SameLine(); MetricsHelpMarker("You can also call ImGui::ShowDebugLogWindow() from your code.")
|
|
_, cfg.ShowIDStackTool = ImGui.Checkbox("Show ID Stack Tool", cfg.ShowIDStackTool == true)
|
|
ImGui.SameLine(); MetricsHelpMarker("You can also call ImGui::ShowIDStackToolWindow() from your code.")
|
|
_, cfg.ShowWindowsBeginOrder = ImGui.Checkbox("Show windows begin order", cfg.ShowWindowsBeginOrder == true)
|
|
_, cfg.ShowWindowsRects = ImGui.Checkbox("Show windows rectangles", cfg.ShowWindowsRects == true)
|
|
ImGui.SameLine()
|
|
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12)
|
|
local changed
|
|
cfg.ShowWindowsRectsType, changed = ImGui.Combo("##show_windows_rect_type", cfg.ShowWindowsRectsType, WRT_Names, #WRT_Names, #WRT_Names)
|
|
cfg.ShowWindowsRects = cfg.ShowWindowsRects or changed
|
|
if cfg.ShowWindowsRects and g.NavWindow ~= nil then
|
|
ImGui.BulletText("'%s':", g.NavWindow.Name)
|
|
ImGui.Indent()
|
|
for rect_n = 0, #WRT_Names - 1 do
|
|
local r = GetWindowRect(g.NavWindow, rect_n)
|
|
ImGui.Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r:GetWidth(), r:GetHeight(), WRT_Names[rect_n + 1])
|
|
end
|
|
ImGui.Unindent()
|
|
end
|
|
_, cfg.ShowTablesRects = ImGui.Checkbox("Show tables rectangles", cfg.ShowTablesRects == true)
|
|
ImGui.SameLine()
|
|
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 12)
|
|
cfg.ShowTablesRectsType, changed = ImGui.Combo("##show_table_rects_type", cfg.ShowTablesRectsType, TRT_Names, #TRT_Names, #TRT_Names)
|
|
cfg.ShowTablesRects = cfg.ShowTablesRects or changed
|
|
_, g.DebugShowGroupRects = ImGui.Checkbox("Show groups rectangles", g.DebugShowGroupRects == true) -- Storing in context as this is used by group code and prefers to be in hot-data
|
|
|
|
ImGui.SeparatorText("Validate")
|
|
_, io.ConfigDebugBeginReturnValueLoop = ImGui.Checkbox("Debug Begin/BeginChild return value", io.ConfigDebugBeginReturnValueLoop == true)
|
|
ImGui.SameLine(); MetricsHelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running.")
|
|
_, cfg.ShowTextEncodingViewer = ImGui.Checkbox("UTF-8 Encoding viewer", cfg.ShowTextEncodingViewer == true)
|
|
ImGui.SameLine(); MetricsHelpMarker("You can also call ImGui::DebugTextEncoding() from your code with a given string to test that your UTF-8 encoding settings are correct.")
|
|
if cfg.ShowTextEncodingViewer then
|
|
MetricsEncodingBuf = MetricsEncodingBuf or DemoBuf("", 64)
|
|
ImGui.SetNextItemWidth(-FLT_MIN)
|
|
ImGui.InputText("##DebugTextEncodingBuf", MetricsEncodingBuf, 64)
|
|
local str = DemoBufStr(MetricsEncodingBuf)
|
|
if str ~= "" then ImGui.DebugTextEncoding(str) end
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Windows
|
|
if ImGui.TreeNode("Windows", "Windows (%d)", g.Windows.Size) then
|
|
-- ImGui.SetNextItemOpen(true, ImGuiCond.Once)
|
|
ImGui.DebugNodeWindowsList(g.Windows, "By display order")
|
|
ImGui.DebugNodeWindowsList(g.WindowsFocusOrder, "By focus order (root windows)")
|
|
if ImGui.TreeNode("By submission order (begin stack)") then
|
|
-- Here we display windows in their submitted order/hierarchy, however note that the Begin stack doesn't constitute a Parent<>Child relationship!
|
|
local tmp = {}
|
|
for _, window in g.Windows:iter() do
|
|
if window.LastFrameActive + 1 >= g.FrameCount then tmp[#tmp + 1] = window end
|
|
end
|
|
table.sort(tmp, function(a, b) return a.BeginOrderWithinContext < b.BeginOrderWithinContext end)
|
|
ImGui.DebugNodeWindowsListByBeginStackParent(tmp, 1, #tmp, nil)
|
|
ImGui.TreePop()
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- DrawLists
|
|
local drawlist_count = 0
|
|
for _, viewport in g.Viewports:iter() do drawlist_count = drawlist_count + viewport.DrawDataP.CmdLists.Size end
|
|
if ImGui.TreeNode("DrawLists", "DrawLists (%d)", drawlist_count) then
|
|
_, cfg.ShowDrawCmdMesh = ImGui.Checkbox("Show ImDrawCmd mesh when hovering", cfg.ShowDrawCmdMesh ~= false)
|
|
_, cfg.ShowDrawCmdBoundingBoxes = ImGui.Checkbox("Show ImDrawCmd bounding boxes when hovering", cfg.ShowDrawCmdBoundingBoxes ~= false)
|
|
for _, viewport in g.Viewports:iter() do
|
|
local has = false
|
|
for _, draw_list in viewport.DrawDataP.CmdLists:iter() do
|
|
if not has then ImGui.Text("Active DrawLists in Viewport #%d, ID: 0x%08X", viewport.Idx or 0, viewport.ID) end
|
|
has = true
|
|
ImGui.DebugNodeDrawList(nil, viewport, draw_list, "DrawList")
|
|
end
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Viewports
|
|
if ImGui.TreeNode("Viewports", "Viewports (%d)", g.Viewports.Size) then
|
|
ImGui.SetNextItemOpen(true, ImGuiCond.Once)
|
|
if ImGui.TreeNode("Windows Minimap") then
|
|
ImGui.RenderViewportsThumbnails()
|
|
ImGui.TreePop()
|
|
end
|
|
cfg.HighlightViewportID = 0
|
|
for _, viewport in g.Viewports:iter() do ImGui.DebugNodeViewport(viewport) end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Details for Fonts
|
|
for i, atlas in g.FontAtlases:iter() do
|
|
if ImGui.TreeNode(tostring(atlas), "Fonts (%d), Textures (%d)", atlas.Fonts.Size, atlas.TexList.Size) then
|
|
ImGui.ShowFontAtlas(atlas)
|
|
ImGui.TreePop()
|
|
end
|
|
end
|
|
|
|
-- Details for Popups
|
|
if ImGui.TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size) then
|
|
for _, popup_data in g.OpenPopupStack:iter() do
|
|
-- As it's difficult to interact with tree nodes while popups are open, we display everything inline.
|
|
local window = popup_data.Window
|
|
local W = ImGuiWindowFlags
|
|
ImGui.BulletText("PopupID: %08x, Window: '%s' (%s%s), RestoreNavWindow '%s', ParentWindow '%s'", popup_data.PopupId, window and window.Name or "NULL",
|
|
(window and bit32.band(window.Flags, W.ChildWindow) ~= 0) and "Child;" or "", (window and bit32.band(window.Flags, W.ChildMenu) ~= 0) and "Menu;" or "",
|
|
popup_data.RestoreNavWindow and popup_data.RestoreNavWindow.Name or "NULL", (window and window.ParentWindow) and window.ParentWindow.Name or "NULL")
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Details for TabBars
|
|
if ImGui.TreeNode("TabBars", "Tab Bars (%d)", g.TabBars.AliveCount) then
|
|
for n = 0, g.TabBars:GetMapSize() - 1 do
|
|
local tab_bar = g.TabBars:GetByIndex(n)
|
|
if tab_bar then
|
|
ImGui.PushID(n)
|
|
ImGui.DebugNodeTabBar(tab_bar, "TabBar")
|
|
ImGui.PopID()
|
|
end
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Details for Tables
|
|
if ImGui.TreeNode("Tables", "Tables (%d)", g.Tables.AliveCount) then
|
|
ForEachTable(g, ImGui.DebugNodeTable)
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Details for Docking
|
|
if ImGui.TreeNode("Docking") then
|
|
local dc = g.DockContext
|
|
_, MetricsDockRootOnly = ImGui.Checkbox("List root nodes", MetricsDockRootOnly)
|
|
_, cfg.ShowDockingNodes = ImGui.Checkbox("Ctrl shows window dock info", cfg.ShowDockingNodes == true)
|
|
if ImGui.SmallButton("Clear nodes") then ImGui.DockContextClearNodes(g, 0, true) end
|
|
ImGui.SameLine()
|
|
if ImGui.SmallButton("Rebuild all") then dc.WantFullRebuild = true end
|
|
local ids = {}
|
|
for id in pairs(dc.Nodes) do ids[#ids + 1] = id end
|
|
table.sort(ids)
|
|
for _, id in ipairs(ids) do
|
|
local node = dc.Nodes[id]
|
|
if node and (not MetricsDockRootOnly or node:IsRootNode()) then ImGui.DebugNodeDockNode(node, "Node") end
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Settings
|
|
if ImGui.TreeNode("Settings") then
|
|
if ImGui.SmallButton("Clear") then ImGui.ClearIniSettings() end
|
|
ImGui.SameLine()
|
|
if ImGui.SmallButton("Save to memory") then ImGui.SaveIniSettingsToMemory() end
|
|
ImGui.SameLine()
|
|
ImGui.TextUnformatted(io.IniFilename and ("\"" .. io.IniFilename .. "\"") or "<NULL>")
|
|
ImGui.Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer)
|
|
if ImGui.TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size) then
|
|
for _, handler in g.SettingsHandlers:iter() do ImGui.BulletText("\"%s\"", handler.TypeName) end
|
|
ImGui.TreePop()
|
|
end
|
|
if ImGui.TreeNode("SettingsWindows", "Settings packed data: Windows: %d entries", g.SettingsWindows.Size) then
|
|
for _, settings in g.SettingsWindows:iter() do ImGui.DebugNodeWindowSettings(settings) end
|
|
ImGui.TreePop()
|
|
end
|
|
if ImGui.TreeNode("SettingsTables", "Settings packed data: Tables: %d entries", g.SettingsTables.Size) then
|
|
for _, settings in g.SettingsTables:iter() do ImGui.DebugNodeTableSettings(settings, nil) end
|
|
ImGui.TreePop()
|
|
end
|
|
if ImGui.TreeNode("SettingsDocking", "Settings packed data: Docking") then
|
|
local dc = g.DockContext
|
|
ImGui.Text("In SettingsWindows:")
|
|
for _, settings in g.SettingsWindows:iter() do
|
|
if (settings.DockId or 0) ~= 0 then ImGui.BulletText("Window '%s' -> DockId %08X DockOrder=%d", settings.Name or "", settings.DockId, settings.DockOrder or -1) end
|
|
end
|
|
ImGui.Text("In SettingsNodes:")
|
|
for _, settings in dc.NodesSettings:iter() do
|
|
local selected_tab_name = nil
|
|
if (settings.SelectedTabId or 0) ~= 0 then
|
|
local window = ImGui.FindWindowByID(settings.SelectedTabId)
|
|
selected_tab_name = window and window.Name or nil
|
|
end
|
|
ImGui.BulletText("Node %08X, Parent %08X, SelectedTab %08X ('%s')", settings.ID, settings.ParentNodeId or 0, settings.SelectedTabId or 0, selected_tab_name or ((settings.SelectedTabId or 0) ~= 0 and "N/A" or ""))
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
local ini = g.SettingsIniData or ""
|
|
if ImGui.TreeNode("SettingsIniData", "Settings unpacked data (.ini): %d bytes", #ini) then
|
|
ImGui.InputTextMultiline("##Ini", DemoBuf(ini, #ini + 1), #ini + 1, ImVec2(-FLT_MIN, ImGui.GetTextLineHeight() * 20), ImGuiInputTextFlags.ReadOnly)
|
|
ImGui.TreePop()
|
|
end
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Inputs
|
|
if ImGui.TreeNode("Inputs") then
|
|
ImGui.Text("KEYBOARD/GAMEPAD/MOUSE KEYS")
|
|
-- User code should never have to go through such hoops! You can generally iterate between ImGuiKey.NamedKey_BEGIN and ImGuiKey.NamedKey_END.
|
|
ImGui.Indent()
|
|
ImGui.Text("Keys down:")
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
if ImGui.IsKeyDown(key) then ImGui.SameLine(); ImGui.Text("\"%s\"", ImGui.GetKeyName(key)) end
|
|
end
|
|
ImGui.Text("Keys pressed:")
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
if ImGui.IsKeyPressed(key) then ImGui.SameLine(); ImGui.Text("\"%s\"", ImGui.GetKeyName(key)) end
|
|
end
|
|
ImGui.Text("Keys released:")
|
|
for key = ImGuiKey.NamedKey_BEGIN, ImGuiKey.NamedKey_END - 1 do
|
|
if ImGui.IsKeyReleased(key) then ImGui.SameLine(); ImGui.Text("\"%s\"", ImGui.GetKeyName(key)) end
|
|
end
|
|
ImGui.Text("Keys mods: %s%s%s%s", io.KeyCtrl and "Ctrl " or "", io.KeyShift and "Shift " or "", io.KeyAlt and "Alt " or "", io.KeySuper and "Super " or "")
|
|
ImGui.Unindent()
|
|
ImGui.Text("MOUSE STATE")
|
|
ImGui.Indent()
|
|
if ImGui.IsMousePosValid() then ImGui.Text("Mouse pos: (%g, %g)", io.MousePos.x, io.MousePos.y) else ImGui.Text("Mouse pos: <INVALID>") end
|
|
ImGui.Text("Mouse delta: (%g, %g)", io.MouseDelta.x, io.MouseDelta.y)
|
|
ImGui.Text("Mouse down:")
|
|
for i = 0, 2 do if ImGui.IsMouseDown(i) then ImGui.SameLine(); ImGui.Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]) end end
|
|
ImGui.Text("Mouse clicked:")
|
|
for i = 0, 2 do if ImGui.IsMouseClicked(i) then ImGui.SameLine(); ImGui.Text("b%d (%d)", i, io.MouseClickedCount[i]) end end
|
|
ImGui.Text("Mouse released:")
|
|
for i = 0, 2 do if ImGui.IsMouseReleased(i) then ImGui.SameLine(); ImGui.Text("b%d", i) end end
|
|
ImGui.Text("Mouse wheel: %.1f", io.MouseWheel)
|
|
ImGui.Text("MouseStationaryTimer: %.2f", g.MouseStationaryTimer)
|
|
ImGui.Unindent()
|
|
ImGui.Text("MOUSE WHEELING")
|
|
ImGui.Indent()
|
|
ImGui.Text("WheelingWindow: '%s'", g.WheelingWindow and g.WheelingWindow.Name or "NULL")
|
|
ImGui.Text("WheelingWindowReleaseTimer: %.2f", g.WheelingWindowReleaseTimer or 0)
|
|
ImGui.Text("WheelingAxisAvg[] = { %.3f, %.3f }", g.WheelingAxisAvg.x, g.WheelingAxisAvg.y)
|
|
ImGui.Unindent()
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Internal state
|
|
if ImGui.TreeNode("Internal state") then
|
|
local function N(w) return w and w.Name or "NULL" end
|
|
ImGui.Text("WINDOWING")
|
|
ImGui.Indent()
|
|
ImGui.Text("HoveredWindow: '%s'", N(g.HoveredWindow))
|
|
ImGui.Text("HoveredWindow->Root: '%s'", g.HoveredWindow and N(g.HoveredWindow.RootWindowDockTree) or "NULL")
|
|
ImGui.Text("HoveredWindowUnderMovingWindow: '%s'", N(g.HoveredWindowUnderMovingWindow))
|
|
ImGui.Text("HoveredDockNode: 0x%08X", g.DebugHoveredDockNode and g.DebugHoveredDockNode.ID or 0)
|
|
ImGui.Text("MovingWindow: '%s'", N(g.MovingWindow))
|
|
ImGui.Unindent()
|
|
ImGui.Text("ITEMS")
|
|
ImGui.Indent()
|
|
ImGui.Text("ActiveId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, B(g.ActiveIdAllowOverlap))
|
|
ImGui.Text("ActiveIdWindow: '%s'", N(g.ActiveIdWindow))
|
|
ImGui.Text("HoveredId: 0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredIdPreviousFrame, g.HoveredIdTimer, B(g.HoveredIdAllowOverlap)) -- Not displaying g.HoveredId as it is update mid-frame
|
|
ImGui.Text("HoverItemDelayId: 0x%08X, Timer: %.2f, ClearTimer: %.2f", g.HoverItemDelayId or 0, g.HoverItemDelayTimer or 0, g.HoverItemDelayClearTimer or 0)
|
|
ImGui.Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\"", B(g.DragDropActive), g.DragDropPayload.SourceId or 0, tostring(g.DragDropPayload.DataType or ""))
|
|
ImGui.Unindent()
|
|
ImGui.Text("NAV,FOCUS")
|
|
ImGui.Indent()
|
|
ImGui.Text("NavWindow: '%s'", N(g.NavWindow))
|
|
ImGui.Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer)
|
|
ImGui.Text("NavActive: %d, NavVisible: %d", B(io.NavActive), B(io.NavVisible))
|
|
ImGui.Text("NavActivateId/DownId/PressedId: %08X/%08X/%08X", g.NavActivateId, g.NavActivateDownId or 0, g.NavActivatePressedId or 0)
|
|
ImGui.Text("NavActivateFlags: %04X", g.NavActivateFlags or 0)
|
|
ImGui.Text("NavCursorVisible: %d, NavHighlightItemUnderNav: %d", B(g.NavCursorVisible), B(g.NavHighlightItemUnderNav))
|
|
ImGui.Text("NavFocusScopeId = 0x%08X", g.NavFocusScopeId or 0)
|
|
ImGui.Text("NavWindowingTarget: '%s'", N(g.NavWindowingTarget))
|
|
ImGui.Unindent()
|
|
ImGui.TreePop()
|
|
end
|
|
|
|
-- Overlay: Display windows Rectangles and Begin Order
|
|
if cfg.ShowWindowsRects or cfg.ShowWindowsBeginOrder then
|
|
for _, window in g.Windows:iter() do
|
|
if window.WasActive then
|
|
local draw_list = FG(window)
|
|
if cfg.ShowWindowsRects then
|
|
local r = GetWindowRect(window, cfg.ShowWindowsRectsType)
|
|
draw_list:AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255))
|
|
end
|
|
if cfg.ShowWindowsBeginOrder and bit32.band(window.Flags, ImGuiWindowFlags.ChildWindow) == 0 then
|
|
local font_size = ImGui.GetFontSize()
|
|
draw_list:AddRectFilled(window.Pos, window.Pos + ImVec2(font_size, font_size), IM_COL32(200, 100, 100, 255))
|
|
draw_list:AddText(window.Pos, IM_COL32(255, 255, 255, 255), tostring(window.BeginOrderWithinContext))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Overlay: Display Tables Rectangles
|
|
if cfg.ShowTablesRects then
|
|
ForEachTable(g, function(tbl)
|
|
if tbl.LastFrameActive < g.FrameCount - 1 then return end
|
|
local draw_list = FG(tbl.OuterWindow)
|
|
if cfg.ShowTablesRectsType >= TRT_ColumnsRect then
|
|
for column_n = 0, tbl.ColumnsCount - 1 do
|
|
local r = GetTableRect(tbl, cfg.ShowTablesRectsType, column_n)
|
|
local hot = (tbl.HoveredColumnBody == column_n)
|
|
draw_list:AddRect(r.Min, r.Max, hot and IM_COL32(255, 255, 128, 255) or IM_COL32(255, 0, 128, 255), 0.0, hot and 3.0 or 1.0)
|
|
end
|
|
else
|
|
local r = GetTableRect(tbl, cfg.ShowTablesRectsType, -1)
|
|
draw_list:AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255))
|
|
end
|
|
end)
|
|
end
|
|
|
|
ImGui.End()
|
|
return p_open
|
|
end
|
|
end --[[ [SECTION] METRICS/DEBUGGER WINDOW, DEBUG LOG, ID STACK TOOL, FONT DEBUG ]]
|
|
|
|
return true -- [Roblox] ModuleScripts must return exactly one value
|