[UI] Image post-processing and full presentation/window rework
[GPU] Add FXAA post-processing [UI] Add FidelityFX FSR and CAS post-processing [UI] Add blue noise dithering from 10bpc to 8bpc [GPU] Apply the DC PWL gamma ramp closer to the spec, supporting fully white color [UI] Allow the GPU CP thread to present on the host directly, bypassing the UI thread OS paint event [UI] Allow variable refresh rate (or tearing) [UI] Present the newest frame (restart) on DXGI [UI] Replace GraphicsContext with a far more advanced Presenter with more coherent surface connection and UI overlay state management [UI] Connect presentation to windows via the Surface class, not native window handles [Vulkan] Switch to simpler Vulkan setup with no instance/device separation due to interdependencies and to pass fewer objects around [Vulkan] Lower the minimum required Vulkan version to 1.0 [UI/GPU] Various cleanup, mainly ComPtr usage [UI] Support per-monitor DPI awareness v2 on Windows [UI] DPI-scale Dear ImGui [UI] Replace the remaining non-detachable window delegates with unified window event and input listeners [UI] Allow listeners to safely destroy or close the window, and to register/unregister listeners without use-after-free and the ABA problem [UI] Explicit Z ordering of input listeners and UI overlays, top-down for input, bottom-up for drawing [UI] Add explicit window lifecycle phases [UI] Replace Window virtual functions with explicit desired state, its application, actual state, its feedback [UI] GTK: Apply the initial size to the drawing area [UI] Limit internal UI frame rate to that of the monitor [UI] Hide the cursor using a timer instead of polling due to no repeated UI thread paints with GPU CP thread presentation, and only within the window
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2020 Ben Vanik. All rights reserved. *
|
||||
* Copyright 2022 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
@@ -10,209 +10,711 @@
|
||||
#ifndef XENIA_UI_WINDOW_H_
|
||||
#define XENIA_UI_WINDOW_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "xenia/base/delegate.h"
|
||||
#include "xenia/base/platform.h"
|
||||
#include "xenia/ui/graphics_context.h"
|
||||
#include "xenia/ui/menu_item.h"
|
||||
#include "xenia/ui/presenter.h"
|
||||
#include "xenia/ui/surface.h"
|
||||
#include "xenia/ui/ui_event.h"
|
||||
#include "xenia/ui/virtual_key.h"
|
||||
#include "xenia/ui/window_listener.h"
|
||||
#include "xenia/ui/windowed_app_context.h"
|
||||
|
||||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
typedef void* NativePlatformHandle;
|
||||
typedef void* NativeWindowHandle;
|
||||
|
||||
class ImGuiDrawer;
|
||||
|
||||
class Window {
|
||||
public:
|
||||
static std::unique_ptr<Window> Create(WindowedAppContext& app_context_,
|
||||
const std::string& title);
|
||||
// Transitions between these phases is sequential and looped (and >= and <=
|
||||
// can be used for openness checks, for closedness checks ! of >= and <= is
|
||||
// needed due to looping), with the exception of kDeleting that may be entered
|
||||
// from any other state (with an assertion for that not being done during
|
||||
// kOpening though as that's extremely dangerous and would require a lot of
|
||||
// handling in OpenImpl on all platforms) as the Window object may be deleted
|
||||
// externally at any moment, and the Window will have no control anymore when
|
||||
// that happens. Another exception is that the window may be closed in the
|
||||
// platform during kOpening - in this case, it will skip kOpenBeforeClosing
|
||||
// and go directly to kClosing and beyond.
|
||||
enum class Phase {
|
||||
// The window hasn't been opened yet, or has been fully closed and can be
|
||||
// reopened.
|
||||
// No native window - external state updates change only the desired state,
|
||||
// don't go to the implementation immediately.
|
||||
// No actual state.
|
||||
// Listeners are not called.
|
||||
kClosedOpenable,
|
||||
// OpenImpl is being invoked, the implementation is performing initial
|
||||
// setup.
|
||||
// External state changes are functionally near-impossible as OpenImpl
|
||||
// mostly isn't able to communicate with external Xenia code that may have
|
||||
// any effect on the Window, to avoid interference during the native window
|
||||
// setup which may be pretty complex - and also, this is the phase in which
|
||||
// the native window is being created, so there's no guarantee that the
|
||||
// native window exists throughout this phase.
|
||||
// However, it's still not strictly enforceable that OpenImpl will not cause
|
||||
// any interaction with the Window - it may happen, for instance, if
|
||||
// OpenImpl causes the OS to execute the pending functions in the
|
||||
// WindowedAppContext that might have been, for example, left over from when
|
||||
// the window was still open last time. Therefore, for the reason of state
|
||||
// consistency within OpenImpl, all external state changes are simply
|
||||
// dropped in this phase.
|
||||
// However, the desired state may be updated by the implementation during
|
||||
// OpenImpl still, but via the Update functions (if the implementation or
|
||||
// the OS, for instance, clamps the size of the window during creation, or
|
||||
// refuses to enter fullscreen).
|
||||
// Actual state is first populated during this phase, and it's readable
|
||||
// (with its level of completeness at the specific point in time) for the
|
||||
// internal purposes of the implementation.
|
||||
// Listeners are not called so the implementation can perform all the setup
|
||||
// without outer interference (the common code calls some to let the
|
||||
// listeners be aware of the initial state when the window enters kOpen
|
||||
// anyway).
|
||||
// Note: Closing may occur in the platform during kOpening - skip
|
||||
// kOpenBeforeClosing in this case while closing.
|
||||
kOpening,
|
||||
// Fully interactive.
|
||||
// The native window exists, external state changes are applied immediately
|
||||
// (or at least immediately requested to be applied shortly after, but the
|
||||
// implementation must make sure that, for instance, if SetFullscreen is
|
||||
// successfully called, but the window will actually enter fullscreen only
|
||||
// at the next platform event loop tick, IsFullscreen will start returning
|
||||
// true immediately).
|
||||
// The desired state can be updated as feedback from the implementation.
|
||||
// Actual state can be retrieved.
|
||||
// Listeners are called.
|
||||
// The only state in which a non-null Surface can be created (it's destroyed
|
||||
// after entering kClosing).
|
||||
kOpen,
|
||||
// OnBeforeClose is being invoked.
|
||||
// The native window still exists, this is mostly like kOpen, but with no
|
||||
// way of creating a Surface (it's becoming destroyed in this state so the
|
||||
// native window can be destroyed safely in the next phase) or recursively
|
||||
// requesting closing - for consistency during closing within the platform,
|
||||
// state changes also behave like in kOpen.
|
||||
// Listeners are still called as normal, primarily because this is where the
|
||||
// OnClosing listener function is invoked, but reopening the window from a
|
||||
// listener is not possible for state consistency during closing.
|
||||
kOpenBeforeClosing,
|
||||
// OnBeforeClose has completed, but OnAfterClose hasn't been called yet for
|
||||
// the implementation to confirm that it has finished destroying the native
|
||||
// window being closed. This state exists to prevent the situation in which
|
||||
// the Window is somehow reopened in the middle of the implementation's
|
||||
// internal work in closing.
|
||||
// The implementation must detach from the native window and destroy it in
|
||||
// this phase.
|
||||
// The native window is being destroyed - external state updates change only
|
||||
// the desired state, don't go to the implementation immediately.
|
||||
// Actual state is still queryable for internal purposes of the
|
||||
// implementation.
|
||||
// Listeners are not called.
|
||||
kClosing,
|
||||
// OnBeforeClose has completed, but the close has occurred from a listener,
|
||||
// and the call stack of listeners still hasn't been exited.
|
||||
// This state exists to prevent the situation in which the Window is being
|
||||
// closed and then reopened in the middle of the implementation's internal
|
||||
// work in event handlers. For example, let's assume putting a window (HWND)
|
||||
// in the fullscreen state requires removing window decorations
|
||||
// (SetWindowLong) and resizing the window (SetWindowPos), both being able
|
||||
// to invoke the resize handler (WM_SIZE) and thus the resize listener. In
|
||||
// this case, consider the following situation:
|
||||
// - SetWindowLong called for HWND.
|
||||
// - WM_SIZE arrives.
|
||||
// - Listener's OnResize called.
|
||||
// - Listener's OnResize does RequestClose, destroying the current HWND.
|
||||
// - Listener's OnResize does Open, creating a new HWND.
|
||||
// - SetWindowPos called for HWND, which is different now.
|
||||
// Here, SetWindowLong will be called for one native window, but then it
|
||||
// will be replaced, and SetWindowPos will be called for a different one.
|
||||
// No native window - external state updates change only the desired state,
|
||||
// don't go to the implementation immediately.
|
||||
// No actual state.
|
||||
// Listeners are not called.
|
||||
kClosedLeavingListeners,
|
||||
// The destructor has been called - should transition into this before doing
|
||||
// anything not only in the common, but in the implementation's destructor
|
||||
// as well. The transition must be done regardless of the previous phase as
|
||||
// there's no way the Window can stop its destruction from now on. This is
|
||||
// mostly similar to kClosedLeavingListeners, except there's no way to leave
|
||||
// this phase.
|
||||
kDeleting,
|
||||
};
|
||||
|
||||
enum class CursorVisibility {
|
||||
kVisible,
|
||||
// Temporarily revealed, hidden if not interacting with the mouse.
|
||||
kAutoHidden,
|
||||
kHidden,
|
||||
};
|
||||
|
||||
static std::unique_ptr<Window> Create(WindowedAppContext& app_context,
|
||||
const std::string_view title,
|
||||
uint32_t desired_logical_width,
|
||||
uint32_t desired_logical_height);
|
||||
|
||||
virtual ~Window();
|
||||
|
||||
WindowedAppContext& app_context() const { return app_context_; }
|
||||
|
||||
virtual NativePlatformHandle native_platform_handle() const = 0;
|
||||
virtual NativeWindowHandle native_handle() const = 0;
|
||||
|
||||
MenuItem* main_menu() const { return main_menu_.get(); }
|
||||
void set_main_menu(std::unique_ptr<MenuItem> main_menu) {
|
||||
main_menu_ = std::move(main_menu);
|
||||
OnMainMenuChange();
|
||||
Phase phase() const { return phase_; }
|
||||
bool HasActualState() const {
|
||||
// kOpening - for internal use by the implementation.
|
||||
// kOpen, kOpenBeforeClosing - for both external and internal use.
|
||||
// kClosing - for internal use by the implementation.
|
||||
return phase_ >= Phase::kOpening && phase_ <= Phase::kClosing;
|
||||
}
|
||||
|
||||
virtual void EnableMainMenu() = 0;
|
||||
virtual void DisableMainMenu() = 0;
|
||||
void AddListener(WindowListener* listener);
|
||||
void RemoveListener(WindowListener* listener);
|
||||
void AddInputListener(WindowInputListener* listener, size_t z_order);
|
||||
void RemoveInputListener(WindowInputListener* listener);
|
||||
|
||||
const std::string& title() const { return title_; }
|
||||
virtual bool set_title(const std::string_view title) {
|
||||
if (title == title_) {
|
||||
return false;
|
||||
// `false` is returned only in case of an error while trying to perform the
|
||||
// platform window opening. If the window is already open, or just can't be
|
||||
// reopened in the current phase (as in this case it's assumed that the outer
|
||||
// will handle this situation properly and won't, for instance, leave a
|
||||
// process without windows - it will quit the application in OnBeforeClose of
|
||||
// the window closing of which was initiated before or even during opening,
|
||||
// for instance), `true` is returned. The functions of WindowListeners will be
|
||||
// called only for a newly opened platform window - and the listeners may
|
||||
// close or even destroy the window, in which case this function will still
|
||||
// return `true` to differentiate from an actual error - if it's really
|
||||
// necessary that the platform window is open after the call, check phase()
|
||||
// after calling.
|
||||
bool Open();
|
||||
// The call may or may not close the window immediately, depending on the
|
||||
// platform (phase() may still return an open phase, and events may still be
|
||||
// sent after the call). Use phase() to check if closing has actually
|
||||
// happened immediately.
|
||||
void RequestClose() {
|
||||
// Don't allow external close requests during opening for state consistency
|
||||
// inside OpenImpl (if an internal close happens during OpenImpl, the
|
||||
// implementation will be aware of that at least), and don't allow closing
|
||||
// twice.
|
||||
if (phase_ != Phase::kOpen) {
|
||||
return;
|
||||
}
|
||||
title_ = title;
|
||||
return true;
|
||||
RequestCloseImpl();
|
||||
// Must not doing anything else with *this as callbacks might have been
|
||||
// triggered during closing (if it has actually even happened), and the
|
||||
// Window might have been deleted.
|
||||
}
|
||||
|
||||
virtual bool SetIcon(const void* buffer, size_t size) = 0;
|
||||
// The `public` state setters are for calling from outside.
|
||||
// The implementation must use the public getters to obtain the desired state
|
||||
// while applying, but for updating the actual state, or for overriding the
|
||||
// desired state, the `protected` On*Update functions must be used (overall
|
||||
// the On* functions are for the implementation's feedback).
|
||||
|
||||
virtual uint32_t GetMediumDpi() const { return 96; }
|
||||
uint32_t GetDpi() const {
|
||||
uint32_t dpi = GetLatestDpiImpl();
|
||||
return dpi ? dpi : GetMediumDpi();
|
||||
}
|
||||
|
||||
// Round trips are not guaranteed to return the same results.
|
||||
static constexpr uint32_t ConvertSizeDpi(uint32_t size, uint32_t new_dpi,
|
||||
uint32_t old_dpi) {
|
||||
// Always rounding up to prevent zero sizes (unless the input is zero) as
|
||||
// well as gaps at the edge.
|
||||
return uint32_t((uint64_t(size) * new_dpi + (old_dpi - 1)) / old_dpi);
|
||||
}
|
||||
uint32_t SizeToLogical(uint32_t size) const {
|
||||
return ConvertSizeDpi(size, GetMediumDpi(), GetDpi());
|
||||
}
|
||||
uint32_t SizeToPhysical(uint32_t size) const {
|
||||
return ConvertSizeDpi(size, GetDpi(), GetMediumDpi());
|
||||
}
|
||||
static constexpr int32_t ConvertPositionDpi(int32_t position,
|
||||
uint32_t new_dpi,
|
||||
uint32_t old_dpi) {
|
||||
// Rounding to the nearest mostly similar to Windows MulDiv.
|
||||
// Plus old_dpi / 2 for positive values, minus old_dpi / 2 for negative
|
||||
// values for consistent rounding for both positive and negative values (as
|
||||
// the `/` operator rounds towards zero).
|
||||
// Example:
|
||||
// (-3 - 1) / 3 == -1
|
||||
// (-2 - 1) / 3 == -1
|
||||
// (-1 - 1) / 3 == 0
|
||||
// ---
|
||||
// (0 + 1) / 3 == 0
|
||||
// (1 + 1) / 3 == 0
|
||||
// (2 + 1) / 3 == 1
|
||||
return int32_t((int64_t(position) * new_dpi +
|
||||
int32_t(old_dpi >> 1) * (position < 0 ? -1 : 1)) /
|
||||
old_dpi);
|
||||
}
|
||||
int32_t PositionToLogical(int32_t position) const {
|
||||
return ConvertPositionDpi(position, GetMediumDpi(), GetDpi());
|
||||
}
|
||||
int32_t PositionToPhysical(int32_t position) const {
|
||||
return ConvertPositionDpi(position, GetDpi(), GetMediumDpi());
|
||||
}
|
||||
|
||||
// The desired logical size of the window when it's not maximized, regardless
|
||||
// of the current state of the window (maximized, fullscreen, etc.)
|
||||
// The implementation may update it, for instance, to clamp it, or when the
|
||||
// user resizes a non-maximized window.
|
||||
uint32_t GetDesiredLogicalWidth() const { return desired_logical_width_; }
|
||||
uint32_t GetDesiredLogicalHeight() const { return desired_logical_height_; }
|
||||
|
||||
// 0 width or height may be returned even in case of an open window with a
|
||||
// valid non-zero-area surface depending on the platform.
|
||||
uint32_t GetActualPhysicalWidth() const {
|
||||
return HasActualState() ? actual_physical_width_ : 0;
|
||||
}
|
||||
uint32_t GetActualPhysicalHeight() const {
|
||||
return HasActualState() ? actual_physical_height_ : 0;
|
||||
}
|
||||
uint32_t GetActualLogicalWidth() const {
|
||||
return SizeToLogical(GetActualPhysicalWidth());
|
||||
}
|
||||
uint32_t GetActualLogicalHeight() const {
|
||||
return SizeToLogical(GetActualPhysicalHeight());
|
||||
}
|
||||
|
||||
// Desired state stored by the common Window, modifiable both externally and
|
||||
// by the implementation (including from SetFullscreen itself).
|
||||
virtual bool IsFullscreen() const { return fullscreen_; }
|
||||
void SetFullscreen(bool new_fullscreen);
|
||||
|
||||
// Desired state stored by the common Window, externally modifiable, read-only
|
||||
// in the implementation.
|
||||
const std::string& GetTitle() const { return title_; }
|
||||
void SetTitle(const std::string_view new_title);
|
||||
|
||||
// Desired state stored in a platform-dependent way in the implementation,
|
||||
// externally modifiable, read-only by the implementation unless from the
|
||||
// LoadAndApplyIcon implementation. The icon is in Windows .ico format.
|
||||
// Provide null buffer and / or zero size to reset the icon.
|
||||
void SetIcon(const void* buffer, size_t size);
|
||||
void ResetIcon() { SetIcon(nullptr, 0); }
|
||||
|
||||
virtual bool CaptureMouse() = 0;
|
||||
virtual bool ReleaseMouse() = 0;
|
||||
// Desired state stored by the common Window, externally modifiable, read-only
|
||||
// in the implementation.
|
||||
void SetMainMenu(std::unique_ptr<MenuItem> new_main_menu);
|
||||
void CompleteMainMenuItemsUpdate();
|
||||
void SetMainMenuEnabled(bool enabled);
|
||||
|
||||
virtual bool is_fullscreen() const { return false; }
|
||||
virtual void ToggleFullscreen(bool fullscreen) {}
|
||||
|
||||
virtual bool is_bordered() const { return false; }
|
||||
virtual void set_bordered(bool enabled) {}
|
||||
|
||||
virtual int get_medium_dpi() const { return 96; }
|
||||
virtual int get_dpi() const { return get_medium_dpi(); }
|
||||
virtual float get_dpi_scale() const {
|
||||
return float(get_dpi()) / float(get_medium_dpi());
|
||||
// Desired state stored by the common Window, externally modifiable, read-only
|
||||
// in the implementation.
|
||||
bool IsMouseCaptureRequested() const {
|
||||
return mouse_capture_request_count_ != 0;
|
||||
}
|
||||
void CaptureMouse();
|
||||
void ReleaseMouse();
|
||||
|
||||
bool has_focus() const { return has_focus_; }
|
||||
virtual void set_focus(bool value) { has_focus_ = value; }
|
||||
// Desired state stored by the common Window, externally modifiable, read-only
|
||||
// in the implementation.
|
||||
CursorVisibility GetCursorVisibility() const { return cursor_visibility_; }
|
||||
// Setting this to kAutoHidden from any _other_ visibility should hide the
|
||||
// cursor immediately - for instance, if the external code wants to auto-hide
|
||||
// the cursor in fullscreen, to allow going into the fullscreen mode to hide
|
||||
// the cursor instantly.
|
||||
void SetCursorVisibility(CursorVisibility new_cursor_visibility);
|
||||
|
||||
bool is_cursor_visible() const { return is_cursor_visible_; }
|
||||
virtual void set_cursor_visible(bool value) { is_cursor_visible_ = value; }
|
||||
bool HasFocus() const { return HasActualState() ? has_focus_ : false; }
|
||||
// May be applied in a delayed way or dropped at all, HasFocus will not
|
||||
// necessarily be true immediately.
|
||||
void Focus();
|
||||
|
||||
// TODO(Triang3l): Don't scale for guest output - use physical pixels. Use
|
||||
// logical pixels only for the immediate drawer.
|
||||
int32_t width() const { return width_; }
|
||||
int32_t height() const { return height_; }
|
||||
int32_t scaled_width() const { return int32_t(width_ * get_dpi_scale()); }
|
||||
int32_t scaled_height() const { return int32_t(height_ * get_dpi_scale()); }
|
||||
// TODO(Triang3l): A resize function, primarily for snapping externally to
|
||||
// 1280x720, 1920x1080, and other 1:1 resolutions. It will need to resize the
|
||||
// window (to a desired logical size - the actual physical size is entirely
|
||||
// the feedback of the implementation) in the normal state, and possibly also
|
||||
// un-maximize (and possibly un-fullscreen) it (but this choice will possibly
|
||||
// need to be exposed to the caller). Because it's currently not needed, it's
|
||||
// not implemented to avoid platform-specific complexities regarding
|
||||
// maximization, DPI, etc.
|
||||
|
||||
virtual void Resize(int32_t width, int32_t height) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
void SetPresenter(Presenter* presenter);
|
||||
|
||||
// Request repainting of the surface. Can be called from non-UI threads as
|
||||
// long as they know the Surface exists and isn't in the middle of being
|
||||
// changed to another (the synchronization of this fact between the UI thread
|
||||
// and the caller thread must be done externally through OnSurfaceChanged).
|
||||
void RequestPaint() {
|
||||
if (presenter_surface_) {
|
||||
RequestPaintImpl();
|
||||
}
|
||||
}
|
||||
virtual void Resize(int32_t left, int32_t top, int32_t right,
|
||||
int32_t bottom) {
|
||||
width_ = right - left;
|
||||
height_ = bottom - top;
|
||||
}
|
||||
|
||||
GraphicsContext* context() const { return context_.get(); }
|
||||
ImGuiDrawer* imgui_drawer() const { return imgui_drawer_.get(); }
|
||||
bool is_imgui_input_enabled() const { return is_imgui_input_enabled_; }
|
||||
void set_imgui_input_enabled(bool value);
|
||||
|
||||
void AttachListener(WindowListener* listener);
|
||||
void DetachListener(WindowListener* listener);
|
||||
|
||||
virtual bool Initialize() { return true; }
|
||||
void set_context(std::unique_ptr<GraphicsContext> context) {
|
||||
context_ = std::move(context);
|
||||
if (context_) {
|
||||
MakeReady();
|
||||
void RequestPresenterUIPaintFromUIThread() {
|
||||
if (presenter_) {
|
||||
presenter_->RequestUIPaintFromUIThread();
|
||||
}
|
||||
}
|
||||
|
||||
void Layout();
|
||||
virtual void Invalidate();
|
||||
|
||||
virtual void Close() = 0;
|
||||
|
||||
public:
|
||||
Delegate<UIEvent*> on_closing;
|
||||
Delegate<UIEvent*> on_closed;
|
||||
|
||||
Delegate<UIEvent*> on_painting;
|
||||
Delegate<UIEvent*> on_paint;
|
||||
Delegate<UIEvent*> on_painted;
|
||||
Delegate<UIEvent*> on_context_lost;
|
||||
|
||||
Delegate<FileDropEvent*> on_file_drop;
|
||||
|
||||
Delegate<KeyEvent*> on_key_down;
|
||||
Delegate<KeyEvent*> on_key_up;
|
||||
Delegate<KeyEvent*> on_key_char;
|
||||
|
||||
Delegate<MouseEvent*> on_mouse_down;
|
||||
Delegate<MouseEvent*> on_mouse_move;
|
||||
Delegate<MouseEvent*> on_mouse_up;
|
||||
Delegate<MouseEvent*> on_mouse_wheel;
|
||||
|
||||
protected:
|
||||
Window(WindowedAppContext& app_context, const std::string& title);
|
||||
// The receiver, which must never be instantiated in the Window object itself
|
||||
// (rather, usually it should be created as a local variable, because only
|
||||
// LIFO-ordered creation and deletion of these is supported), that allows
|
||||
// deletion of the Window from within an event handler (which may invoke a
|
||||
// WindowListener, and window listeners are allowed to destroy windows; also
|
||||
// they may execute, for instance, the functions requested to be executed in
|
||||
// the UI thread in the WindowedAppContext, which are also allowed to destroy
|
||||
// windows - because if the former wasn't allowed, the latter would be
|
||||
// required to destroy windows as a result of UI interaction) to be caught by
|
||||
// functions inside the Window in order to stop interacting with `*this` and
|
||||
// returning after this happens.
|
||||
// Note that the receivers are signaled in the *end* of the destruction of the
|
||||
// common Window, when truly nothing can be done with it anymore, so it's safe
|
||||
// to assume that right after the creation of the WindowDestructionReceiver,
|
||||
// it will still be in an unsignaled state even if it's used somewhere in the
|
||||
// destructor. The reason is that the users of the WindowDestructionReceiver
|
||||
// are expected to stop accessing the Window *immediately* once
|
||||
// IsWindowDestroyed becomes `true`, and to leave it in a potentially
|
||||
// indeterminate state - but the code executed subsequently in the destructor
|
||||
// may still use that state meaningfully.
|
||||
class WindowDestructionReceiver {
|
||||
public:
|
||||
explicit WindowDestructionReceiver(Window* window) : window_(window) {
|
||||
if (window_) {
|
||||
outer_receiver_ = window_->innermost_destruction_receiver_;
|
||||
window_->innermost_destruction_receiver_ = this;
|
||||
}
|
||||
}
|
||||
|
||||
void ForEachListener(std::function<void(WindowListener*)> fn);
|
||||
void TryForEachListener(std::function<bool(WindowListener*)> fn);
|
||||
~WindowDestructionReceiver() {
|
||||
// If the window is not null, removal from the stack must happen
|
||||
// regardless of `phase_ == Phase::kDeleting`, because the window
|
||||
// destructor iterates the receivers after EnterDestructor(), and if the
|
||||
// receiver is not removed in this case, the destructor will do
|
||||
// use-after-free.
|
||||
if (window_) {
|
||||
// Only LIFO order is supported (normally through RAII).
|
||||
assert_true(window_->innermost_destruction_receiver_ == this);
|
||||
window_->innermost_destruction_receiver_ = outer_receiver_;
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool MakeReady();
|
||||
bool IsWindowDestroyed() const { return window_ == nullptr; }
|
||||
|
||||
virtual bool OnCreate();
|
||||
virtual void OnMainMenuChange();
|
||||
virtual void OnClose();
|
||||
virtual void OnDestroy();
|
||||
// Helper functions for common usages of the receiver. Unlike
|
||||
// IsWindowDestroyed, these, however, may return false immediately on
|
||||
// creation.
|
||||
// Primarily for the implementation (most importantly its native event
|
||||
// handler), to stop interacting with the native window given that it was
|
||||
// possible before the function call it's guarded with.
|
||||
bool IsWindowDestroyedOrClosed() const {
|
||||
return IsWindowDestroyed() || window_->IsClosed();
|
||||
}
|
||||
// For guarding Apply* calls if one state setter needs to make multiple of
|
||||
// them (or just detecting if it's okay to call Apply*).
|
||||
bool IsWindowDestroyedOrStateInapplicable() const {
|
||||
return IsWindowDestroyed() || !window_->CanApplyState();
|
||||
}
|
||||
bool IsWindowDestroyedOrListenersUncallable() const {
|
||||
return IsWindowDestroyed() || !window_->CanSendEventsToListeners();
|
||||
}
|
||||
|
||||
virtual void OnDpiChanged(UIEvent* e);
|
||||
virtual void OnResize(UIEvent* e);
|
||||
virtual void OnLayout(UIEvent* e);
|
||||
virtual void OnPaint(UIEvent* e);
|
||||
virtual void OnFileDrop(FileDropEvent* e);
|
||||
private:
|
||||
// The Window must set window_ to nullptr in its destructor.
|
||||
friend Window;
|
||||
Window* window_;
|
||||
WindowDestructionReceiver* outer_receiver_ = nullptr;
|
||||
};
|
||||
|
||||
virtual void OnVisible(UIEvent* e);
|
||||
virtual void OnHidden(UIEvent* e);
|
||||
// Like in the Windows Media Player.
|
||||
// A more modern Windows example, Movies & TV in Windows 11 21H2, has 5000,
|
||||
// but it's too long especially for highly dynamic games.
|
||||
// Implementations may use different values according to the platform's UX
|
||||
// conventions.
|
||||
static constexpr uint32_t kDefaultCursorAutoHideMilliseconds = 3333;
|
||||
|
||||
virtual void OnGotFocus(UIEvent* e);
|
||||
virtual void OnLostFocus(UIEvent* e);
|
||||
Window(WindowedAppContext& app_context, const std::string_view title,
|
||||
uint32_t desired_logical_width, uint32_t desired_logical_height);
|
||||
|
||||
virtual void OnKeyDown(KeyEvent* e);
|
||||
virtual void OnKeyUp(KeyEvent* e);
|
||||
virtual void OnKeyChar(KeyEvent* e);
|
||||
// If implementation-specific destruction happens, should be called in the
|
||||
// beginning of the implementation's destructor so the implementation can
|
||||
// destroy the platform window without doing something unsafe in destruction.
|
||||
void EnterDestructor() {
|
||||
phase_ = Phase::kDeleting;
|
||||
// Disconnect from the surface before destroying the window behind it.
|
||||
OnSurfaceChanged(false);
|
||||
}
|
||||
|
||||
virtual void OnMouseDown(MouseEvent* e);
|
||||
virtual void OnMouseMove(MouseEvent* e);
|
||||
virtual void OnMouseUp(MouseEvent* e);
|
||||
virtual void OnMouseWheel(MouseEvent* e);
|
||||
// For an open window, the implementation should return the current DPI for
|
||||
// the window. For a non-open one, it should be the closest approximation,
|
||||
// such as the last DPI from an existing window, the system DPI, or just the
|
||||
// medium DPI (0 returned from it will also be treated as medium DPI).
|
||||
virtual uint32_t GetLatestDpiImpl() const { return GetMediumDpi(); }
|
||||
|
||||
void OnKeyPress(KeyEvent* e, bool is_down, bool is_char);
|
||||
// Deletion of the window may (and must) not happen in OpenImpl, the listeners
|
||||
// are deferred, so there's no need to use WindowDestructionReceiver in it.
|
||||
// In case of failure, the implementation must not leave itself in an
|
||||
// indeterminate state, so another attempt to open the window can be made.
|
||||
// The implementation must apply the following desired state if it needs it,
|
||||
// directly (not via Set* methods as they will be dropped during OpenImpl
|
||||
// since the window is not fully open yet):
|
||||
// - Title (GetTitle()).
|
||||
// - Icon (from the last LoadAndApplyIcon call).
|
||||
// - Main menu (GetMainMenu()) and its enablement.
|
||||
// - Desired logical size (GetDesiredLogicalWidth() / Height(), taking into
|
||||
// account that the main menu, during the initial opening, should not be
|
||||
// included in this size - it specifies the client area that painting will
|
||||
// be done to), within the capabilities of the platform (may be clamped by
|
||||
// the OS, for instance - in this case, OnDesiredLogicalSizeUpdate may be
|
||||
// called from within OpenImpl to store the clamped size for later).
|
||||
// - Fullscreen (GetFullscreen()) - however, if possible, the calculations
|
||||
// that would normally be done for a non-fullscreen window in OpenImpl
|
||||
// should also be done if entering fullscreen, including the menu-related
|
||||
// ones - first, the usual windowed geometry calculations should be done,
|
||||
// and then fullscreen should be entered; but preferably still entering
|
||||
// fullscreen before actually showing a visible window to the user for a
|
||||
// seamless transition.
|
||||
// - Mouse capture (IsMouseCaptureRequested()).
|
||||
// - Cursor visibility (GetCursorVisibility()).
|
||||
// Also, as a result of the OpenImpl call, these function should be called
|
||||
// (immediately from within OpenImpl directly or indirectly through the native
|
||||
// event handling, or shortly after during the UI main loop) to provide the
|
||||
// initial actual state to the common Window code so it returns the correct
|
||||
// values:
|
||||
// - OnActualSizeUpdate, at least if the window isn't opened as not yet
|
||||
// visible on screen (otherwise the size will be assumed to be 0x0 until the
|
||||
// next size update caused by something likely requiring user interaction).
|
||||
// - OnFocusUpdate, at least if the focus has been obtained (otherwise the
|
||||
// window will be assumed to be not in focus until it goes into the focus
|
||||
// again).
|
||||
// Also, if some of the desired state that may be updated by the
|
||||
// implementation could not be applied (such as the fullscreen mode), and
|
||||
// certain values of the implementation state are either normally
|
||||
// differentiated within the implementation or are just meaningful considering
|
||||
// the platform's defaults (for instance, the platform inherently supports
|
||||
// only fullscreen, possibly doesn't have a concept of windows at all), it's
|
||||
// recommended to call the appropriate On*Update functions to update the
|
||||
// desired state to the actual one.
|
||||
virtual bool OpenImpl() = 0;
|
||||
virtual void RequestCloseImpl() = 0;
|
||||
|
||||
// Apply* are called only if CanApplyState() is true (unless the function
|
||||
// should do more than just applying, such as also updating the desired state
|
||||
// in a platform-dependent way - see each individual function).
|
||||
// ApplyNew* means that the value has actually been changed to something
|
||||
// different than it was previously.
|
||||
virtual void ApplyNewFullscreen() {}
|
||||
virtual void ApplyNewTitle() {}
|
||||
// can_apply_state_in_current_phase whether the window is in a life cycle
|
||||
// phase that would normally accept Apply calls (the native window surely
|
||||
// exists), since this function may be called in closed states too to update
|
||||
// the desired icon (since it's stored in the implementation) - the
|
||||
// implementation may, however, ignore it and use a more granular check of the
|
||||
// existence of the native window and the safety of updating the icon for
|
||||
// better internal state consistency. The icon is in Windows .ico format. If
|
||||
// the buffer is null or the size is 0, the icon should be reset to the
|
||||
// default one. Returns whether the icon has been updated successfully.
|
||||
virtual void LoadAndApplyIcon(const void* buffer, size_t size,
|
||||
bool can_apply_state_in_current_phase) {}
|
||||
MenuItem* GetMainMenu() const { return main_menu_.get(); }
|
||||
// May be called to add, replace or remove the main menu.
|
||||
virtual void ApplyNewMainMenu(MenuItem* old_main_menu) {}
|
||||
// If there's main menu, and state can be applied, will be called to make the
|
||||
// implementation's state consistent with the new state of the MenuItems of
|
||||
// the main menu after changes have been made to them.
|
||||
virtual void CompleteMainMenuItemsUpdateImpl() {}
|
||||
// Will be called even if capturing while the mouse is already assumed to be
|
||||
// captured, in case something has released it in the OS.
|
||||
virtual void ApplyNewMouseCapture() {}
|
||||
virtual void ApplyNewMouseRelease() {}
|
||||
virtual void ApplyNewCursorVisibility(
|
||||
CursorVisibility old_cursor_visibility) {}
|
||||
// If state can be applied, this is called to request bringing the window into
|
||||
// focus (and once that's done by the OS, update the actual focus state). Does
|
||||
// nothing otherwise (focus can't be requested before the window is open, a
|
||||
// closed window is always assumed to be not in focus).
|
||||
virtual void FocusImpl() {}
|
||||
|
||||
Presenter* presenter() const { return presenter_; }
|
||||
bool HasSurface() const { return presenter_surface_ != nullptr; }
|
||||
// If new_surface_potentially_exists is false, creation of the new surface for
|
||||
// the window won't be updated, and it may be called from the destructor (via
|
||||
// EnterDestructor to destroy the surface before destroying what it depends
|
||||
// on) as no virtual functions (including CreateSurface) will be called.
|
||||
// This function is nonvirtual itself for this reason as well.
|
||||
void OnSurfaceChanged(bool new_surface_potentially_exists);
|
||||
// Called only for an open window.
|
||||
virtual std::unique_ptr<Surface> CreateSurfaceImpl(
|
||||
Surface::TypeFlags allowed_types) = 0;
|
||||
// Called only if the Surface exists.
|
||||
virtual void RequestPaintImpl() = 0;
|
||||
|
||||
// Will also disconnect the surface if needed.
|
||||
void OnBeforeClose(WindowDestructionReceiver& destruction_receiver);
|
||||
void OnAfterClose();
|
||||
|
||||
// These functions may usually also be called as part of the opening process
|
||||
// from within OpenImpl (directly or through the platform event handler
|
||||
// invoked during it) to actualize the state for the newly createad window,
|
||||
// especially if it's different than the desired one.
|
||||
void OnDpiChanged(UISetupEvent& e,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
void OnMonitorUpdate(MonitorUpdateEvent& e);
|
||||
// For calling when the platform changes something in the non-maximized,
|
||||
// non-fullscreen size of the window.
|
||||
void OnDesiredLogicalSizeUpdate(uint32_t new_desired_logical_width,
|
||||
uint32_t new_desired_logical_height) {
|
||||
desired_logical_width_ = new_desired_logical_width;
|
||||
desired_logical_height_ = new_desired_logical_height;
|
||||
}
|
||||
// If the size of the client area is the same as the currently assumed one
|
||||
// (the desired / last size for a newly opened / reopened window, the last
|
||||
// actual size for an already open window), does nothing and returns false.
|
||||
// Otherwise, updates the size, notifies what depends on the size about the
|
||||
// change, and returns true. Not storing the new size in the UISetupEvent
|
||||
// because a resize listener may request another resize, in which case it will
|
||||
// be outdated - listeners must query the new physical size from the window
|
||||
// explicitly.
|
||||
bool OnActualSizeUpdate(uint32_t new_physical_width,
|
||||
uint32_t new_physical_height,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
void OnDesiredFullscreenUpdate(bool new_fullscreen) {
|
||||
fullscreen_ = new_fullscreen;
|
||||
}
|
||||
void OnFocusUpdate(bool new_has_focus,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
|
||||
// Pass true as force_paint in case the platform can't retain the image from
|
||||
// the previous paint so it won't be skipped if there are no content updates.
|
||||
void OnPaint(bool force_paint = false);
|
||||
|
||||
void OnFileDrop(FileDropEvent& e,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
|
||||
void OnKeyDown(KeyEvent& e, WindowDestructionReceiver& destruction_receiver);
|
||||
void OnKeyUp(KeyEvent& e, WindowDestructionReceiver& destruction_receiver);
|
||||
void OnKeyChar(KeyEvent& e, WindowDestructionReceiver& destruction_receiver);
|
||||
|
||||
void OnMouseDown(MouseEvent& e,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
void OnMouseMove(MouseEvent& e,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
void OnMouseUp(MouseEvent& e,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
void OnMouseWheel(MouseEvent& e,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
|
||||
private:
|
||||
struct ListenerIterationContext {
|
||||
explicit ListenerIterationContext(ListenerIterationContext* outer_context,
|
||||
size_t first_index = 0)
|
||||
: outer_context(outer_context), next_index(first_index) {}
|
||||
// To support nested listener calls, in case a listener does some
|
||||
// interaction with the window that results in more events being triggered
|
||||
// (such as calling Windows API functions that return a result from a
|
||||
// message handled by a window, rather that simply enqueueing the message).
|
||||
ListenerIterationContext* outer_context;
|
||||
// Using indices, not iterators, because after the erasure, the adjustment
|
||||
// must be done for the vector element indices that would be in the iterator
|
||||
// range that would be invalidated.
|
||||
size_t next_index;
|
||||
};
|
||||
|
||||
struct InputListenerIterationContext {
|
||||
explicit InputListenerIterationContext(
|
||||
InputListenerIterationContext* outer_context,
|
||||
std::multimap<size_t, WindowInputListener*>::const_reverse_iterator
|
||||
first_iterator,
|
||||
size_t first_z_order = SIZE_MAX)
|
||||
: outer_context(outer_context),
|
||||
next_iterator(first_iterator),
|
||||
current_z_order(first_z_order) {}
|
||||
// To support nested listener calls.
|
||||
InputListenerIterationContext* outer_context;
|
||||
// Reverse iterator because input handlers with a higher Z order index may
|
||||
// correspond to what's displayed on top of what has a lower Z order index,
|
||||
// so what's higher may consum the event.
|
||||
std::multimap<size_t, WindowInputListener*>::const_reverse_iterator
|
||||
next_iterator;
|
||||
size_t current_z_order;
|
||||
};
|
||||
|
||||
// If the window is closed, the platform native window is either being
|
||||
// destroyed, or doesn't exist anymore, and thus it's in a non-interactive
|
||||
// state.
|
||||
bool IsClosed() const {
|
||||
return phase_ < Phase::kOpening && phase_ > Phase::kOpenBeforeClosing;
|
||||
}
|
||||
|
||||
bool CanApplyState() const {
|
||||
// In kOpening, OpenImpl itself pulls the desired state itself and applies
|
||||
// it, the Apply* functions can't be called and are unsafe to call because
|
||||
// the implementation is an incomplete state, with the platform window
|
||||
// potentially not existing. In kOpenBeforeClosing, as the native window
|
||||
// still hasn't been destroyed and it can receive new state, still allowing
|
||||
// applying new state for more consistency between the desired and the
|
||||
// actual state during the final listener invocation.
|
||||
return phase_ >= Phase::kOpen && phase_ <= Phase::kOpenBeforeClosing;
|
||||
}
|
||||
|
||||
bool CanSendEventsToListeners() const {
|
||||
return phase_ >= Phase::kOpen && phase_ <= Phase::kOpenBeforeClosing;
|
||||
}
|
||||
// The listeners may delete the Window - check the destruction receiver after
|
||||
// calling and stop doing anything accessing *this if that happens.
|
||||
void SendEventToListeners(std::function<void(WindowListener*)> fn,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
void PropagateEventThroughInputListeners(
|
||||
std::function<bool(WindowInputListener*)> fn,
|
||||
WindowDestructionReceiver& destruction_receiver);
|
||||
|
||||
std::unique_ptr<Surface> CreateSurface(Surface::TypeFlags allowed_types) {
|
||||
// If opening, surface creation is deferred until all the initial setup has
|
||||
// completed. Destruction of the surface is also a part of the closing
|
||||
// process in OnBeforeClose.
|
||||
if (phase_ != Phase::kOpen) {
|
||||
return nullptr;
|
||||
}
|
||||
return CreateSurfaceImpl(allowed_types);
|
||||
}
|
||||
|
||||
WindowedAppContext& app_context_;
|
||||
std::unique_ptr<MenuItem> main_menu_;
|
||||
std::string title_;
|
||||
#ifdef XE_PLATFORM_GNU_LINUX
|
||||
// GTK must have a default value here that isn't 0
|
||||
// TODO(Triang3l): Cleanup and unify this. May need to get the first resize
|
||||
// message on various platforms.
|
||||
int32_t width_ = 1280;
|
||||
int32_t height_ = 720;
|
||||
#else
|
||||
int32_t width_ = 0;
|
||||
int32_t height_ = 0;
|
||||
#endif
|
||||
bool has_focus_ = true;
|
||||
bool is_cursor_visible_ = true;
|
||||
bool is_imgui_input_enabled_ = false;
|
||||
|
||||
std::unique_ptr<GraphicsContext> context_;
|
||||
std::unique_ptr<ImGuiDrawer> imgui_drawer_;
|
||||
|
||||
uint32_t frame_count_ = 0;
|
||||
uint32_t fps_ = 0;
|
||||
uint64_t fps_update_time_ticks_ = 0;
|
||||
uint64_t fps_frame_count_ = 0;
|
||||
uint64_t last_paint_time_ticks_ = 0;
|
||||
|
||||
bool modifier_shift_pressed_ = false;
|
||||
bool modifier_cntrl_pressed_ = false;
|
||||
bool modifier_alt_pressed_ = false;
|
||||
bool modifier_super_pressed_ = false;
|
||||
Phase phase_ = Phase::kClosedOpenable;
|
||||
WindowDestructionReceiver* innermost_destruction_receiver_ = nullptr;
|
||||
|
||||
// All currently-attached listeners that get event notifications.
|
||||
bool in_listener_loop_ = false;
|
||||
std::vector<WindowListener*> listeners_;
|
||||
std::vector<WindowListener*> pending_listener_attaches_;
|
||||
std::vector<WindowListener*> pending_listener_detaches_;
|
||||
// Ordered by the Z order, and then by the time of addition (but executed in
|
||||
// reverse order).
|
||||
// Note: All the iteration logic involving this Z ordering must be the same as
|
||||
// in drawing (in the UI drawers in the Presenter), but in reverse.
|
||||
std::multimap<size_t, WindowInputListener*> input_listeners_;
|
||||
// Linked list-based stacks of the contexts of the listener iterations
|
||||
// currently being done, usually allocated on the stack.
|
||||
ListenerIterationContext* innermost_listener_iteration_context_ = nullptr;
|
||||
InputListenerIterationContext* innermost_input_listener_iteration_context_ =
|
||||
nullptr;
|
||||
|
||||
uint32_t desired_logical_width_ = 0;
|
||||
uint32_t desired_logical_height_ = 0;
|
||||
// Set by the implementation via OnActualSizeUpdate (from OpenImpl or from the
|
||||
// platform resize handler).
|
||||
uint32_t actual_physical_width_ = 0;
|
||||
uint32_t actual_physical_height_ = 0;
|
||||
|
||||
bool fullscreen_ = false;
|
||||
|
||||
std::string title_;
|
||||
|
||||
std::unique_ptr<MenuItem> main_menu_;
|
||||
|
||||
uint32_t mouse_capture_request_count_ = 0;
|
||||
|
||||
CursorVisibility cursor_visibility_ = CursorVisibility::kVisible;
|
||||
|
||||
bool has_focus_ = false;
|
||||
|
||||
Presenter* presenter_ = nullptr;
|
||||
std::unique_ptr<Surface> presenter_surface_;
|
||||
// Whether currently in InPaint to prevent recursive painting in case it's
|
||||
// triggered somehow from within painting again, because painting is much more
|
||||
// complex than just a small state update, and recursive painting is
|
||||
// completely unsupported by the Presenter.
|
||||
bool is_painting_ = false;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
|
||||
Reference in New Issue
Block a user