// ---------------------------------------------------------------------------- // - Open3D: www.open3d.org - // ---------------------------------------------------------------------------- // The MIT License (MIT) // // Copyright (c) 2018 www.open3d.org // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // ---------------------------------------------------------------------------- #include "Open3D/GUI/Window.h" #include #include #include #include // so we can examine the current context #include #include #include #include #include "Open3D/GUI/Application.h" #include "Open3D/GUI/Button.h" #include "Open3D/GUI/Dialog.h" #include "Open3D/GUI/ImguiFilamentBridge.h" #include "Open3D/GUI/Label.h" #include "Open3D/GUI/Layout.h" #include "Open3D/GUI/Menu.h" #include "Open3D/GUI/Native.h" #include "Open3D/GUI/SceneWidget.h" #include "Open3D/GUI/Theme.h" #include "Open3D/GUI/Util.h" #include "Open3D/GUI/Widget.h" #include "Open3D/Utility/Console.h" #include "Open3D/Visualization/Rendering/Filament/FilamentEngine.h" #include "Open3D/Visualization/Rendering/Filament/FilamentRenderer.h" using namespace open3d::gui::util; // ---------------------------------------------------------------------------- namespace open3d { namespace gui { namespace { static constexpr int CENTERED_X = -10000; static constexpr int CENTERED_Y = -10000; static constexpr int AUTOSIZE_WIDTH = 0; static constexpr int AUTOSIZE_HEIGHT = 0; // Assumes the correct ImGuiContext is current void UpdateImGuiForScaling(float new_scaling) { ImGuiStyle& style = ImGui::GetStyle(); // FrameBorderSize is not adjusted (we want minimal borders) style.FrameRounding *= new_scaling; } int MouseButtonFromGLFW(int button) { switch (button) { case GLFW_MOUSE_BUTTON_LEFT: return int(MouseButton::LEFT); case GLFW_MOUSE_BUTTON_RIGHT: return int(MouseButton::RIGHT); case GLFW_MOUSE_BUTTON_MIDDLE: return int(MouseButton::MIDDLE); case GLFW_MOUSE_BUTTON_4: return int(MouseButton::BUTTON4); case GLFW_MOUSE_BUTTON_5: return int(MouseButton::BUTTON5); default: return int(MouseButton::NONE); } } int KeymodsFromGLFW(int glfw_mods) { int keymods = 0; if (glfw_mods & GLFW_MOD_SHIFT) { keymods |= int(KeyModifier::SHIFT); } if (glfw_mods & GLFW_MOD_CONTROL) { #if __APPLE__ keymods |= int(KeyModifier::ALT); #else keymods |= int(KeyModifier::CTRL); #endif // __APPLE__ } if (glfw_mods & GLFW_MOD_ALT) { #if __APPLE__ keymods |= int(KeyModifier::META); #else keymods |= int(KeyModifier::ALT); #endif // __APPLE__ } if (glfw_mods & GLFW_MOD_SUPER) { #if __APPLE__ keymods |= int(KeyModifier::CTRL); #else keymods |= int(KeyModifier::META); #endif // __APPLE__ } return keymods; } void ChangeAllRenderQuality( SceneWidget::Quality quality, const std::vector>& children) { for (auto child : children) { auto sw = std::dynamic_pointer_cast(child); if (sw) { sw->SetRenderQuality(quality); } else { if (child->GetChildren().size() > 0) { ChangeAllRenderQuality(quality, child->GetChildren()); } } } } } // namespace const int Window::FLAG_TOPMOST = (1 << 0); struct Window::Impl { GLFWwindow* window_ = nullptr; std::string title_; // there is no glfwGetWindowTitle()... // We need these for mouse moves and wheel events. // The only source of ground truth is button events, so the rest of // the time we monitor key up/down events. int mouse_mods_ = 0; // ORed KeyModifiers double last_render_time_ = 0.0; Theme theme_; // so that the font size can be different based on scaling std::unique_ptr renderer_; struct { std::unique_ptr imgui_bridge; ImGuiContext* context = nullptr; ImFont* system_font = nullptr; // reference; owned by imguiContext float scaling = 1.0; } imgui_; std::vector> children_; // Active dialog is owned here. It is not put in the children because // we are going to add it and take it out during draw (since that's // how an immediate mode GUI works) and that involves changing the // children while iterating over it. Also, conceptually it is not a // child, it is a child window, and needs to be on top, which we cannot // guarantee if it is a child widget. std::shared_ptr active_dialog_; std::queue> deferred_until_before_draw_; std::queue> deferred_until_draw_; Widget* mouse_grabber_widget_ = nullptr; // only if not ImGUI widget Widget* focus_widget_ = nullptr; // only used if ImGUI isn't taking keystrokes bool wants_auto_size_and_center_ = false; bool needs_layout_ = true; bool is_resizing_ = false; }; Window::Window(const std::string& title, int flags /*= 0*/) : Window(title, CENTERED_X, CENTERED_Y, AUTOSIZE_WIDTH, AUTOSIZE_HEIGHT) {} Window::Window(const std::string& title, int width, int height, int flags /*= 0*/) : Window(title, CENTERED_X, CENTERED_Y, width, height) {} Window::Window(const std::string& title, int x, int y, int width, int height, int flags /*= 0*/) : impl_(new Window::Impl()) { if (x == CENTERED_X || y == CENTERED_Y || width == AUTOSIZE_WIDTH || height == AUTOSIZE_HEIGHT) { impl_->wants_auto_size_and_center_ = true; } glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // NOTE: Setting alpha and stencil bits to match GLX standard default // values. GLFW sets these internally to 8 and 8 respectively if not // specified which causes problems with Filament on Linux with Nvidia binary // driver glfwWindowHint(GLFW_ALPHA_BITS, 0); glfwWindowHint(GLFW_STENCIL_BITS, 0); #if __APPLE__ glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE); #endif glfwWindowHint(GLFW_VISIBLE, impl_->wants_auto_size_and_center_ ? GLFW_TRUE : GLFW_FALSE); glfwWindowHint(GLFW_FLOATING, ((flags & FLAG_TOPMOST) != 0 ? GLFW_TRUE : GLFW_FALSE)); impl_->window_ = glfwCreateWindow(std::max(10, width), std::max(10, height), title.c_str(), NULL, NULL); impl_->title_ = title; if (x != CENTERED_X || y != CENTERED_Y) { glfwSetWindowPos(impl_->window_, x, y); } glfwSetWindowUserPointer(impl_->window_, this); glfwSetWindowSizeCallback(impl_->window_, ResizeCallback); glfwSetWindowRefreshCallback(impl_->window_, DrawCallback); glfwSetCursorPosCallback(impl_->window_, MouseMoveCallback); glfwSetMouseButtonCallback(impl_->window_, MouseButtonCallback); glfwSetScrollCallback(impl_->window_, MouseScrollCallback); glfwSetKeyCallback(impl_->window_, KeyCallback); glfwSetCharCallback(impl_->window_, CharCallback); glfwSetDropCallback(impl_->window_, DragDropCallback); glfwSetWindowCloseCallback(impl_->window_, CloseCallback); // On single-threaded platforms, Filament's OpenGL context must be current, // not GLFW's context, so create the renderer after the window. // ImGUI creates a bitmap atlas from a font, so we need to have the correct // size when we create it, because we can't change the bitmap without // reloading the whole thing (expensive). float scaling = GetScaling(); impl_->theme_ = Application::GetInstance().GetTheme(); impl_->theme_.font_size *= scaling; impl_->theme_.default_margin *= scaling; impl_->theme_.default_layout_spacing *= scaling; auto& engine = visualization::EngineInstance::GetInstance(); auto& resource_manager = visualization::EngineInstance::GetResourceManager(); impl_->renderer_ = std::make_unique( engine, GetNativeDrawable(), resource_manager); auto& theme = impl_->theme_; // shorter alias impl_->imgui_.context = ImGui::CreateContext(); auto oldContext = MakeDrawContextCurrent(); impl_->imgui_.imgui_bridge = std::make_unique( impl_->renderer_.get(), GetSize()); ImGui::StyleColorsDark(); ImGuiStyle& style = ImGui::GetStyle(); style.WindowPadding = ImVec2(0, 0); style.WindowRounding = 0; style.WindowBorderSize = 0; style.FrameBorderSize = theme.border_width; style.FrameRounding = theme.border_radius; style.Colors[ImGuiCol_WindowBg] = colorToImgui(theme.background_color); style.Colors[ImGuiCol_Text] = colorToImgui(theme.text_color); style.Colors[ImGuiCol_Border] = colorToImgui(theme.border_color); style.Colors[ImGuiCol_Button] = colorToImgui(theme.button_color); style.Colors[ImGuiCol_ButtonHovered] = colorToImgui(theme.button_hover_color); style.Colors[ImGuiCol_ButtonActive] = colorToImgui(theme.button_active_color); style.Colors[ImGuiCol_CheckMark] = colorToImgui(theme.checkbox_check_color); style.Colors[ImGuiCol_FrameBg] = colorToImgui(theme.combobox_background_color); style.Colors[ImGuiCol_FrameBgHovered] = colorToImgui(theme.combobox_hover_color); style.Colors[ImGuiCol_FrameBgActive] = style.Colors[ImGuiCol_FrameBgHovered]; style.Colors[ImGuiCol_SliderGrab] = colorToImgui(theme.slider_grab_color); style.Colors[ImGuiCol_SliderGrabActive] = colorToImgui(theme.slider_grab_color); style.Colors[ImGuiCol_Tab] = colorToImgui(theme.tab_inactive_color); style.Colors[ImGuiCol_TabHovered] = colorToImgui(theme.tab_hover_color); style.Colors[ImGuiCol_TabActive] = colorToImgui(theme.tab_active_color); // If the given font path is invalid, ImGui will silently fall back to // proggy, which is a tiny "pixel art" texture that is compiled into the // library. if (!theme.font_path.empty()) { ImGuiIO& io = ImGui::GetIO(); impl_->imgui_.system_font = io.Fonts->AddFontFromFileTTF( theme.font_path.c_str(), theme.font_size); /*static*/ unsigned char* pixels; int textureW, textureH, bytesPerPx; io.Fonts->GetTexDataAsAlpha8(&pixels, &textureW, &textureH, &bytesPerPx); impl_->imgui_.imgui_bridge->CreateAtlasTextureAlpha8( pixels, textureW, textureH, bytesPerPx); } ImGuiIO& io = ImGui::GetIO(); io.IniFilename = nullptr; #ifdef WIN32 io.ImeWindowHandle = GetNativeDrawable(); #endif // ImGUI's io.KeysDown is indexed by our scan codes, and we fill out // io.KeyMap to map from our code to ImGui's code. io.KeyMap[ImGuiKey_Tab] = KEY_TAB; io.KeyMap[ImGuiKey_LeftArrow] = KEY_LEFT; io.KeyMap[ImGuiKey_RightArrow] = KEY_RIGHT; io.KeyMap[ImGuiKey_UpArrow] = KEY_UP; io.KeyMap[ImGuiKey_DownArrow] = KEY_DOWN; io.KeyMap[ImGuiKey_PageUp] = KEY_PAGEUP; io.KeyMap[ImGuiKey_PageDown] = KEY_PAGEDOWN; io.KeyMap[ImGuiKey_Home] = KEY_HOME; io.KeyMap[ImGuiKey_End] = KEY_END; io.KeyMap[ImGuiKey_Insert] = KEY_INSERT; io.KeyMap[ImGuiKey_Delete] = KEY_DELETE; io.KeyMap[ImGuiKey_Backspace] = KEY_BACKSPACE; io.KeyMap[ImGuiKey_Space] = ' '; io.KeyMap[ImGuiKey_Enter] = KEY_ENTER; io.KeyMap[ImGuiKey_Escape] = KEY_ESCAPE; io.KeyMap[ImGuiKey_A] = 'a'; io.KeyMap[ImGuiKey_C] = 'c'; io.KeyMap[ImGuiKey_V] = 'v'; io.KeyMap[ImGuiKey_X] = 'x'; io.KeyMap[ImGuiKey_Y] = 'y'; io.KeyMap[ImGuiKey_Z] = 'z'; /* io.SetClipboardTextFn = [this](void*, const char* text) { glfwSetClipboardString(this->impl_->window, text); }; io.GetClipboardTextFn = [this](void*) -> const char* { return glfwGetClipboardString(this->impl_->window); }; */ io.ClipboardUserData = nullptr; // Restore the context, in case we are creating a window during a draw. // (This is quite likely, since ImGUI only handles things like button // presses during draw. A file open dialog is likely to create a window // after pressing "Open".) RestoreDrawContext(oldContext); } Window::~Window() { impl_->children_.clear(); // needs to happen before deleting renderer ImGui::SetCurrentContext(impl_->imgui_.context); ImGui::DestroyContext(); impl_->renderer_.reset(); glfwDestroyWindow(impl_->window_); } void* Window::MakeDrawContextCurrent() const { auto old_context = ImGui::GetCurrentContext(); ImGui::SetCurrentContext(impl_->imgui_.context); return old_context; } void Window::RestoreDrawContext(void* oldContext) const { ImGui::SetCurrentContext((ImGuiContext*)oldContext); } void* Window::GetNativeDrawable() const { return open3d::gui::GetNativeDrawable(impl_->window_); } const Theme& Window::GetTheme() const { return impl_->theme_; } visualization::Renderer& Window::GetRenderer() const { return *impl_->renderer_; } Rect Window::GetOSFrame() const { int x, y, w, h; glfwGetWindowPos(impl_->window_, &x, &y); glfwGetWindowSize(impl_->window_, &w, &h); return Rect(x, y, w, h); } void Window::SetOSFrame(const Rect& r) { glfwSetWindowPos(impl_->window_, r.x, r.y); glfwSetWindowSize(impl_->window_, r.width, r.height); } const char* Window::GetTitle() const { return impl_->title_.c_str(); } void Window::SetTitle(const char* title) { impl_->title_ = title; glfwSetWindowTitle(impl_->window_, title); } // Note: can only be called if the ImGUI context is current (that is, // after MakeDrawContextCurrent() has been called), otherwise // ImGUI won't be able to access the font. Size Window::CalcPreferredSize() { Rect bbox(0, 0, 0, 0); for (auto& child : impl_->children_) { auto pref = child->CalcPreferredSize(GetTheme()); Rect r(child->GetFrame().x, child->GetFrame().y, pref.width, pref.height); bbox = bbox.UnionedWith(r); } // Note: we are doing (bbox.GetRight() - 0) NOT (bbox.GetRight() - bbox.x) // (and likewise for height) because the origin of the window is // (0, 0) and anything up/left is clipped. return Size(bbox.GetRight(), bbox.GetBottom()); } void Window::SizeToFit() { // CalcPreferredSize() can only be called while the ImGUI context // is current, but we are probably calling this while setting up the // window. auto auto_size = [this]() { SetSize(CalcPreferredSize()); }; impl_->deferred_until_draw_.push(auto_size); } void Window::SetSize(const Size& size) { // Make sure we do the resize outside of a draw, to avoid unsightly // errors if we happen to do this in the middle of a draw. auto resize = [this, size /*copy*/]() { glfwSetWindowSize(this->impl_->window_, size.width / this->impl_->imgui_.scaling, size.height / this->impl_->imgui_.scaling); // SDL_SetWindowSize() doesn't generate an event, so we need to update // the size ourselves this->OnResize(); }; impl_->deferred_until_before_draw_.push(resize); } Size Window::GetSize() const { uint32_t w, h; glfwGetFramebufferSize(impl_->window_, (int*)&w, (int*)&h); return Size(w, h); } Rect Window::GetContentRect() const { auto size = GetSize(); int menu_height = 0; #if !(GUI_USE_NATIVE_MENUS && defined(__APPLE__)) MakeDrawContextCurrent(); auto menubar = Application::GetInstance().GetMenubar(); if (menubar) { menu_height = menubar->CalcHeight(GetTheme()); } #endif return Rect(0, menu_height, size.width, size.height - menu_height); } float Window::GetScaling() const { #if GLFW_VERSION_MAJOR > 3 || \ (GLFW_VERSION_MAJOR == 3 && GLFW_VERSION_MINOR >= 3) float xscale, yscale; glfwGetWindowContentScale(impl_->window_, &xscale, &yscale); return xscale; #else return 1.0f; #endif // GLFW version >= 3.3 } Point Window::GlobalToWindowCoord(int global_x, int global_y) { int wx, wy; glfwGetWindowPos(impl_->window_, &wx, &wy); return Point(global_y - wx, global_y - wy); } bool Window::IsVisible() const { return glfwGetWindowAttrib(impl_->window_, GLFW_VISIBLE); } void Window::Show(bool vis /*= true*/) { if (vis) { glfwShowWindow(impl_->window_); } else { glfwHideWindow(impl_->window_); } } void Window::Close() { Application::GetInstance().RemoveWindow(this); } void Window::SetNeedsLayout() { impl_->needs_layout_ = true; } void Window::PostRedraw() { PostNativeExposeEvent(impl_->window_); } void Window::RaiseToTop() const { glfwFocusWindow(impl_->window_); } bool Window::IsActiveWindow() const { return glfwGetWindowAttrib(impl_->window_, GLFW_FOCUSED); } void Window::SetFocusWidget(Widget* w) { impl_->focus_widget_ = w; } void Window::AddChild(std::shared_ptr w) { impl_->children_.push_back(w); impl_->needs_layout_ = true; } void Window::ShowDialog(std::shared_ptr dlg) { if (impl_->active_dialog_) { CloseDialog(); } impl_->active_dialog_ = dlg; dlg->OnWillShow(); auto win_size = GetSize(); auto pref = dlg->CalcPreferredSize(GetTheme()); int w = dlg->GetFrame().width; int h = dlg->GetFrame().height; if (w == 0) { w = pref.width; } if (h == 0) { h = pref.height; } w = std::min(w, int(std::round(0.8 * win_size.width))); h = std::min(h, int(std::round(0.8 * win_size.height))); dlg->SetFrame(gui::Rect((win_size.width - w) / 2, (win_size.height - h) / 2, w, h)); dlg->Layout(GetTheme()); } void Window::CloseDialog() { if (impl_->focus_widget_ == impl_->active_dialog_.get()) { SetFocusWidget(nullptr); } impl_->active_dialog_.reset(); } void Window::ShowMessageBox(const char* title, const char* message) { auto em = GetTheme().font_size; auto margins = Margins(GetTheme().default_margin); auto dlg = std::make_shared(title); auto layout = std::make_shared(em, margins); layout->AddChild(std::make_shared