Files
Xenia-Canary/src/xenia/ui/window.cc
2022-07-14 21:13:40 +03:00

743 lines
25 KiB
C++

/**
******************************************************************************
* 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 <algorithm>
#include <iterator>
#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<MenuItem> 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<MenuItem> 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<void(WindowListener*)> 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<bool(WindowInputListener*)> 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