/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/ui/window.h" #include "third_party/imgui/imgui.h" #include "xenia/base/assert.h" #include "xenia/base/clock.h" #include "xenia/base/logging.h" #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/presenter.h" namespace xe { namespace ui { Window::Window(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) : app_context_(app_context), title_(title), desired_logical_width_(desired_logical_width), desired_logical_height_(desired_logical_height) {} Window::~Window() { // In case the implementation didn't need to call EnterDestructor. Though // that was likely a mistake, so placing an assertion. assert_true(phase_ == Phase::kDeleting); EnterDestructor(); if (presenter_) { Presenter* old_presenter = presenter_; // Null the pointer to prevent an infinite loop between // SetWindowSurfaceFromUIThread and SetPresenter calling each other. presenter_ = nullptr; old_presenter->SetWindowSurfaceFromUIThread(nullptr, nullptr); presenter_surface_.reset(); } // Right before destruction has finished, after which no interaction with // *this can be done, notify the destruction receivers that the window is // being destroyed and that *this is not accessible anymore. while (innermost_destruction_receiver_) { innermost_destruction_receiver_->window_ = nullptr; innermost_destruction_receiver_ = innermost_destruction_receiver_->outer_receiver_; } } void Window::AddListener(WindowListener* listener) { assert_not_null(listener); // Check if already added. if (std::find(listeners_.cbegin(), listeners_.cend(), listener) != listeners_.cend()) { return; } listeners_.push_back(listener); } void Window::RemoveListener(WindowListener* listener) { assert_not_null(listener); auto it = std::find(listeners_.cbegin(), listeners_.cend(), listener); if (it == listeners_.cend()) { return; } // Actualize the next listener indices after the erasure from the vector. ListenerIterationContext* iteration_context = innermost_listener_iteration_context_; if (iteration_context) { size_t existing_index = size_t(std::distance(listeners_.cbegin(), it)); while (iteration_context) { if (iteration_context->next_index > existing_index) { --iteration_context->next_index; } iteration_context = iteration_context->outer_context; } } listeners_.erase(it); } void Window::AddInputListener(WindowInputListener* listener, size_t z_order) { assert_not_null(listener); // Check if already added. for (auto it_existing = input_listeners_.rbegin(); it_existing != input_listeners_.rend(); ++it_existing) { if (it_existing->second != listener) { continue; } if (it_existing->first == z_order) { return; } // If removing the listener that is the next in a current listener loop, // skip it (in a multimap, only one element iterator is invalidated). InputListenerIterationContext* iteration_context = innermost_input_listener_iteration_context_; while (iteration_context) { if (iteration_context->next_iterator == it_existing) { ++iteration_context->next_iterator; } iteration_context = iteration_context->outer_context; } input_listeners_.erase(std::prev(it_existing.base())); // FIXME(Triang3l): Changing the Z order of an existing listener while // already executing the listeners may cause listeners to be called twice if // the Z order is lowered from one that has already been processed to one // below the current one. Because nested listener calls are supported, a // single last call index can't be associated with a listener to skip it if // calling twice for the same event (a "not equal to" comparison of the call // indices will result in the skipping being cancelled in the outer loop if // an inner one is done, a "greater than" comparison will cause the inner // loop to effectively terminate all outer ones). } auto it_new = std::prev( std::make_reverse_iterator(input_listeners_.emplace(z_order, listener))); // If adding to layers in between the currently being processed ones (from // highest to lowest) and the previously next, make sure the new listener is // executed too. Execution within one layer, however, happens in the reverse // order of addition, so if adding to the Z layer currently being processed, // the new listener must not be executed within the loop. But, if adding to // the next Z layer after the current one, it must be executed immediately. { InputListenerIterationContext* iteration_context = innermost_input_listener_iteration_context_; while (iteration_context) { if (z_order < iteration_context->current_z_order && (iteration_context->next_iterator == input_listeners_.crend() || z_order >= iteration_context->next_iterator->first)) { iteration_context->next_iterator = it_new; } iteration_context = iteration_context->outer_context; } } } void Window::RemoveInputListener(WindowInputListener* listener) { assert_not_null(listener); for (auto it_existing = input_listeners_.rbegin(); it_existing != input_listeners_.rend(); ++it_existing) { if (it_existing->second != listener) { continue; } // If removing the listener that is the next in a current listener loop, // skip it (in a multimap, only one element iterator is invalidated). InputListenerIterationContext* iteration_context = innermost_input_listener_iteration_context_; while (iteration_context) { if (iteration_context->next_iterator == it_existing) { ++iteration_context->next_iterator; } iteration_context = iteration_context->outer_context; } input_listeners_.erase(std::prev(it_existing.base())); return; } } bool Window::Open() { if (phase_ != Phase::kClosedOpenable) { return true; } // For consistency of the behavior of OpenImpl and the initial On*Update // that should be called as a result of it, reset the actual state to its // defaults for a closed window. // Note that this is performed in Open, not after closing, because there's // only one entry point for opening a window, while closing may be done // different ways - by actually closing, by destroying, or by failing to call // OpenImpl - instead of performing this reset in every possible close case, // just returning these defaults from the actual state getters if // HasActualState is false. actual_physical_width_ = 0; actual_physical_height_ = 0; has_focus_ = false; phase_ = Phase::kOpening; bool platform_open_result = OpenImpl(); if (!platform_open_result) { phase_ = Phase::kClosedOpenable; return false; } if (phase_ != Phase::kOpening) { // The window was closed mid-opening. return true; } phase_ = Phase::kOpen; // Call the listeners (OnOpened with all the new state so the listeners are // aware that they can start interacting with the open Window, and after that, // in case certain listeners don't handle OnOpened, but rather, only need the // more granular callbacks, make sure those callbacks receive the new state // too) for the actual state of the new window (that may be different than the // desired, depending on how the platform has interpreted the desired state). { MonitorUpdateEvent e(this, true, true); OnMonitorUpdate(e); } { UISetupEvent e(this, true); WindowDestructionReceiver destruction_receiver(this); SendEventToListeners([&e](auto listener) { listener->OnOpened(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) { return true; } SendEventToListeners([&e](auto listener) { listener->OnDpiChanged(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) { return true; } SendEventToListeners([&e](auto listener) { listener->OnResize(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) { return true; } if (HasFocus()) { SendEventToListeners([&e](auto listener) { listener->OnGotFocus(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrListenersUncallable()) { return true; } } } // May now try to create a valid surface (though the window may be in a // minimized state without the possibility of creating a surface, but that // will be resolved by the implementation), after OnDpiChanged and OnResize so // nothing related to painting will be making wrong assumptions about the // size. OnSurfaceChanged(true); return true; } void Window::SetFullscreen(bool new_fullscreen) { if (fullscreen_ == new_fullscreen) { return; } fullscreen_ = new_fullscreen; if (!CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); ApplyNewFullscreen(); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::SetTitle(const std::string_view new_title) { if (title_ == new_title) { return; } title_ = new_title; if (!CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); ApplyNewTitle(); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::SetIcon(const void* buffer, size_t size) { bool reset = !buffer || !size; WindowDestructionReceiver destruction_receiver(this); LoadAndApplyIcon(reset ? nullptr : buffer, reset ? 0 : size, CanApplyState()); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::SetMainMenu(std::unique_ptr new_main_menu) { // The primary reason for this comparison (of two unique pointers) is // nullptr == nullptr. if (main_menu_ == new_main_menu) { return; } // Keep the old menu object existing while it's still being detached from // the platform window. std::unique_ptr old_main_menu = std::move(main_menu_); main_menu_ = std::move(new_main_menu); if (!CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); ApplyNewMainMenu(old_main_menu.get()); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::CompleteMainMenuItemsUpdate() { if (!main_menu_ || !CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); CompleteMainMenuItemsUpdateImpl(); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::SetMainMenuEnabled(bool enabled) { if (!main_menu_) { return; } // Not comparing to the actual enabled state, it's a part of the MenuItem, not // the Window. // In case enabling (or even disabling) causes menu-related events (like // pressing) that may execute callbacks potentially destroying the Window via // the outer architecture. WindowDestructionReceiver destruction_receiver(this); main_menu_->SetEnabled(enabled); if (destruction_receiver.IsWindowDestroyed()) { return; } // Modifying the state of the items, notify the implementation so it makes the // displaying of the main menu consistent. CompleteMainMenuItemsUpdate(); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::CaptureMouse() { ++mouse_capture_request_count_; if (!CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); // Call even if capturing while the mouse is already assumed to be captured, // in case something has released it in the OS. ApplyNewMouseCapture(); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::ReleaseMouse() { assert_not_zero(mouse_capture_request_count_); if (!mouse_capture_request_count_) { return; } if (--mouse_capture_request_count_) { return; } if (!CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); ApplyNewMouseRelease(); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::SetCursorVisibility(CursorVisibility new_cursor_visibility) { if (cursor_visibility_ == new_cursor_visibility) { return; } CursorVisibility old_cursor_visibility = cursor_visibility_; cursor_visibility_ = new_cursor_visibility; if (!CanApplyState()) { return; } WindowDestructionReceiver destruction_receiver(this); ApplyNewCursorVisibility(old_cursor_visibility); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::Focus() { if (!CanApplyState() || has_focus_) { return; } WindowDestructionReceiver destruction_receiver(this); FocusImpl(); if (destruction_receiver.IsWindowDestroyedOrStateInapplicable()) { return; } } void Window::SetPresenter(Presenter* presenter) { if (presenter_ == presenter) { return; } if (presenter_) { presenter_->SetWindowSurfaceFromUIThread(nullptr, nullptr); presenter_surface_.reset(); } presenter_ = presenter; if (presenter_) { presenter_surface_ = CreateSurface(presenter_->GetSupportedSurfaceTypes()); presenter_->SetWindowSurfaceFromUIThread(this, presenter_surface_.get()); } } void Window::OnSurfaceChanged(bool new_surface_potentially_exists) { if (!presenter_) { return; } // Detach the presenter from the old surface before attaching to the new one. if (presenter_surface_) { presenter_->SetWindowSurfaceFromUIThread(this, nullptr); presenter_surface_.reset(); } if (!new_surface_potentially_exists) { return; } presenter_surface_ = CreateSurface(presenter_->GetSupportedSurfaceTypes()); if (presenter_surface_) { presenter_->SetWindowSurfaceFromUIThread(this, presenter_surface_.get()); } } void Window::OnBeforeClose(WindowDestructionReceiver& destruction_receiver) { // Because events are not sent from closed windows, and to make sure the // window isn't closed while its surface is still attached to the presenter, // this must be called before doing what constitutes closing in the platform // implementation, not after. bool was_open = phase_ == Phase::kOpen; bool was_open_or_opening = was_open || phase_ == Phase::kOpening; assert_true(was_open_or_opening); if (!was_open_or_opening) { return; } if (was_open) { phase_ = Phase::kOpenBeforeClosing; // If the window was focused, notify the listeners that focus is being lost // because the window is being closed (the implementation usually wouldn't // be sending any events to the listeners after OnClosing even if the OS // actually sends the focus loss event as part of the closing process). OnFocusUpdate(false, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } { UIEvent e(this); SendEventToListeners([&e](auto listener) { listener->OnClosing(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } // Disconnect from the surface without connecting to the new one (after the // listeners so they don't try to reconnect afterwards). OnSurfaceChanged(false); } phase_ = Phase::kClosing; } void Window::OnAfterClose() { assert_true(phase_ == Phase::kClosing); if (phase_ != Phase::kClosing) { return; } phase_ = (innermost_listener_iteration_context_ || innermost_input_listener_iteration_context_) ? Phase::kClosedLeavingListeners : Phase::kClosedOpenable; } void Window::OnDpiChanged(UISetupEvent& e, WindowDestructionReceiver& destruction_receiver) { SendEventToListeners([&e](auto listener) { listener->OnDpiChanged(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnMonitorUpdate(MonitorUpdateEvent& e) { if (presenter_surface_) { presenter_->OnSurfaceMonitorUpdateFromUIThread( e.old_monitor_potentially_disconnected()); } } bool Window::OnActualSizeUpdate( uint32_t new_physical_width, uint32_t new_physical_height, WindowDestructionReceiver& destruction_receiver) { if (actual_physical_width_ == new_physical_width && actual_physical_height_ == new_physical_height) { return false; } actual_physical_width_ = new_physical_width; actual_physical_height_ = new_physical_height; // The listeners may reference the presenter, update the presenter first. if (presenter_surface_) { presenter_->OnSurfaceResizeFromUIThread(); } UISetupEvent e(this); SendEventToListeners([&e](auto listener) { listener->OnResize(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } return true; } void Window::OnFocusUpdate(bool new_has_focus, WindowDestructionReceiver& destruction_receiver) { if (has_focus_ == new_has_focus) { return; } has_focus_ = new_has_focus; UISetupEvent e(this); if (has_focus_) { SendEventToListeners([&e](auto listener) { listener->OnGotFocus(e); }, destruction_receiver); } else { SendEventToListeners([&e](auto listener) { listener->OnLostFocus(e); }, destruction_receiver); } if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnPaint(bool force_paint) { if (is_painting_) { return; } is_painting_ = true; if (presenter_surface_) { presenter_->PaintFromUIThread(force_paint); } is_painting_ = false; } void Window::OnFileDrop(FileDropEvent& e, WindowDestructionReceiver& destruction_receiver) { SendEventToListeners([&e](auto listener) { listener->OnFileDrop(e); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnKeyDown(KeyEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnKeyDown(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnKeyUp(KeyEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnKeyUp(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnKeyChar(KeyEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnKeyChar(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnMouseDown(MouseEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnMouseDown(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnMouseMove(MouseEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnMouseMove(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnMouseUp(MouseEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnMouseUp(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnMouseWheel(MouseEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnMouseWheel(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::OnTouchEvent(TouchEvent& e, WindowDestructionReceiver& destruction_receiver) { PropagateEventThroughInputListeners( [&e](auto listener) { listener->OnTouchEvent(e); return e.is_handled(); }, destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } void Window::SendEventToListeners( std::function fn, WindowDestructionReceiver& destruction_receiver) { if (!CanSendEventsToListeners()) { return; } ListenerIterationContext iteration_context( innermost_listener_iteration_context_); innermost_listener_iteration_context_ = &iteration_context; while (iteration_context.next_index < listeners_.size()) { // iteration_context.next_index may be changed during the execution of the // listener if the list of the listeners is modified by it - don't assume // that after the call iteration_context.next_index will be the same as // iteration_context.next_index + 1 before it. fn(listeners_[iteration_context.next_index++]); if (destruction_receiver.IsWindowDestroyed()) { // The window was destroyed by the listener, can't access anything in // *this anymore, including innermost_listener_iteration_context_ which // has to be left in an indeterminate state. return; } if (!CanSendEventsToListeners()) { // The listener has put the window in a state in which the window can't // send events anymore. break; } } assert_true(innermost_listener_iteration_context_ == &iteration_context); innermost_listener_iteration_context_ = innermost_listener_iteration_context_->outer_context; if (phase_ == Phase::kClosedLeavingListeners && !innermost_listener_iteration_context_ && !innermost_input_listener_iteration_context_) { phase_ = Phase::kClosedOpenable; } } void Window::PropagateEventThroughInputListeners( std::function fn, WindowDestructionReceiver& destruction_receiver) { if (!CanSendEventsToListeners()) { return; } InputListenerIterationContext iteration_context( innermost_input_listener_iteration_context_, input_listeners_.crbegin()); innermost_input_listener_iteration_context_ = &iteration_context; while (iteration_context.next_iterator != input_listeners_.crend()) { // The current iterator may be invalidated, and // iteration_context.next_iterator may be changed, during the execution of // the listener if the list of the listeners is modified by it - don't // assume that after the call iteration_context.next_iterator will be the // same as std::next(iteration_context.next_iterator) before it. iteration_context.current_z_order = iteration_context.next_iterator->first; bool event_handled = fn((iteration_context.next_iterator++)->second); if (destruction_receiver.IsWindowDestroyed()) { // The window was destroyed by the listener, can't access anything in // *this anymore, including innermost_listener_iteration_context_ which // has to be left in an indeterminate state. return; } if (event_handled) { break; } if (!CanSendEventsToListeners()) { // The listener has put the window in a state in which the window can't // send events anymore. break; } } assert_true(innermost_input_listener_iteration_context_ == &iteration_context); innermost_input_listener_iteration_context_ = innermost_input_listener_iteration_context_->outer_context; if (phase_ == Phase::kClosedLeavingListeners && !innermost_listener_iteration_context_ && !innermost_input_listener_iteration_context_) { phase_ = Phase::kClosedOpenable; } } } // namespace ui } // namespace xe