[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:
Triang3l
2022-01-29 13:22:03 +03:00
parent 372bdd3ec9
commit fe3f0f26e4
428 changed files with 75942 additions and 18360 deletions

View File

@@ -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. *
******************************************************************************
*/
@@ -11,11 +11,13 @@
#include <string>
#include <X11/Xlib-xcb.h>
#include <gdk/gdkx.h>
#include <xcb/xcb.h>
#include "xenia/base/assert.h"
#include "xenia/base/clock.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform_linux.h"
#include "xenia/ui/surface_gnulinux.h"
#include "xenia/ui/virtual_key.h"
#include "xenia/ui/window_gtk.h"
@@ -23,384 +25,407 @@ namespace xe {
namespace ui {
std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
const std::string& title) {
return std::make_unique<GTKWindow>(app_context, title);
const std::string_view title,
uint32_t desired_logical_width,
uint32_t desired_logical_height) {
return std::make_unique<GTKWindow>(app_context, title, desired_logical_width,
desired_logical_height);
}
GTKWindow::GTKWindow(WindowedAppContext& app_context, const std::string& title)
: Window(app_context, title) {}
GTKWindow::GTKWindow(WindowedAppContext& app_context,
const std::string_view title,
uint32_t desired_logical_width,
uint32_t desired_logical_height)
: Window(app_context, title, desired_logical_width,
desired_logical_height) {}
GTKWindow::~GTKWindow() {
OnDestroy();
EnterDestructor();
if (window_) {
if (GTK_IS_WIDGET(window_)) {
gtk_widget_destroy(window_);
}
// Set window_ to null to ignore events from now on since this ui::GTKWindow
// is entering an indeterminate state.
GtkWidget* window = window_;
window_ = nullptr;
// Destroying the top-level window also destroys its children.
drawing_area_ = nullptr;
box_ = nullptr;
gtk_widget_destroy(window);
}
}
bool GTKWindow::Initialize() { return OnCreate(); }
gboolean gtk_event_handler(GtkWidget* widget, GdkEvent* event, gpointer data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(data);
switch (event->type) {
case GDK_OWNER_CHANGE:
window->HandleWindowOwnerChange(&(event->owner_change));
break;
case GDK_VISIBILITY_NOTIFY:
window->HandleWindowVisibility(&(event->visibility));
break;
case GDK_KEY_PRESS:
case GDK_KEY_RELEASE:
window->HandleKeyboard(&(event->key));
break;
case GDK_SCROLL:
case GDK_MOTION_NOTIFY:
case GDK_BUTTON_PRESS:
case GDK_BUTTON_RELEASE:
window->HandleMouse(&(event->any));
break;
case GDK_FOCUS_CHANGE:
window->HandleWindowFocus(&(event->focus_change));
break;
case GDK_CONFIGURE:
// Only handle the event for the drawing area so we don't save
// a width and height that includes the menu bar on the full window
if (event->configure.window ==
gtk_widget_get_window(window->drawing_area_)) {
window->HandleWindowResize(&(event->configure));
}
break;
default:
// Do nothing
break;
}
// Propagate the event to other handlers
return GDK_EVENT_PROPAGATE;
}
gboolean draw_callback(GtkWidget* widget, GdkFrameClock* frame_clock,
gpointer data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(data);
window->HandleWindowPaint();
return G_SOURCE_CONTINUE;
}
gboolean close_callback(GtkWidget* widget, gpointer data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(data);
window->Close();
return G_SOURCE_CONTINUE;
}
bool GTKWindow::OnCreate() {
// GTK optionally allows passing argv and argc here for parsing gtk specific
// options. We won't bother
bool GTKWindow::OpenImpl() {
window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_resizable(GTK_WINDOW(window_), true);
gtk_window_set_title(GTK_WINDOW(window_), title_.c_str());
gtk_window_set_default_size(GTK_WINDOW(window_), width_, height_);
// Drawing area is where we will attach our vulkan/gl context
drawing_area_ = gtk_drawing_area_new();
// tick callback is for the refresh rate of the window
gtk_widget_add_tick_callback(drawing_area_, draw_callback,
reinterpret_cast<gpointer>(this), nullptr);
// Attach our event handler to both the main window (for keystrokes) and the
// drawing area (for mouse input, resize event, etc)
g_signal_connect(G_OBJECT(drawing_area_), "event",
G_CALLBACK(gtk_event_handler),
reinterpret_cast<gpointer>(this));
GdkDisplay* gdk_display = gtk_widget_get_display(window_);
assert(GDK_IS_X11_DISPLAY(gdk_display));
connection_ = XGetXCBConnection(gdk_x11_display_get_xdisplay(gdk_display));
gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str());
g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(gtk_event_handler),
reinterpret_cast<gpointer>(this));
// When the window manager kills the window (ie, the user hits X)
g_signal_connect(G_OBJECT(window_), "destroy", G_CALLBACK(close_callback),
reinterpret_cast<gpointer>(this));
// Enable only keyboard events (so no mouse) for the top window
gtk_widget_set_events(window_, GDK_KEY_PRESS | GDK_KEY_RELEASE);
// Enable all events for the drawing area
gtk_widget_add_events(drawing_area_, GDK_ALL_EVENTS_MASK);
// Place the drawing area in a container (which later will hold the menu)
// then let it fill the whole area
// Create the vertical box container for the main menu and the drawing area.
box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_box_pack_end(GTK_BOX(box_), drawing_area_, TRUE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(window_), box_);
// Add the main menu (even if fullscreen was requested, for the initial layout
// calculation).
const GTKMenuItem* main_menu = static_cast<const GTKMenuItem*>(GetMainMenu());
GtkWidget* main_menu_widget = main_menu ? main_menu->handle() : nullptr;
if (main_menu_widget) {
gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, FALSE, FALSE, 0);
}
// Create the drawing area for creating the surface for, which will be the
// client area of the window occupying all the window space not taken by the
// main menu.
drawing_area_ = gtk_drawing_area_new();
gtk_box_pack_end(GTK_BOX(box_), drawing_area_, TRUE, TRUE, 0);
// The desired size is the client (drawing) area size. Let GTK auto-size the
// entire window around it (as well as the width of the menu actually if it
// happens to be bigger - the desired size in the Window will be updated later
// to reflect that).
gtk_widget_set_size_request(drawing_area_, GetDesiredLogicalWidth(),
GetDesiredLogicalHeight());
// Attach the event handlers.
// Keyboard events are processed by the window, mouse events are processed
// within, and by, the drawing (client) area.
gtk_widget_set_events(window_, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK |
GDK_FOCUS_CHANGE_MASK);
gtk_widget_set_events(drawing_area_,
GDK_POINTER_MOTION_MASK | GDK_BUTTON_MOTION_MASK |
GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
GDK_SCROLL_MASK);
g_signal_connect(G_OBJECT(window_), "event",
G_CALLBACK(WindowEventHandlerThunk),
reinterpret_cast<gpointer>(this));
g_signal_connect(G_OBJECT(drawing_area_), "event",
G_CALLBACK(DrawingAreaEventHandlerThunk),
reinterpret_cast<gpointer>(this));
g_signal_connect(G_OBJECT(drawing_area_), "draw", G_CALLBACK(DrawHandler),
reinterpret_cast<gpointer>(this));
// Finally show all the widgets in the window, including the main menu.
gtk_widget_show_all(window_);
return super::OnCreate();
}
// Remove the size request after finishing the initial layout because it makes
// it impossible to make the window smaller.
gtk_widget_set_size_request(drawing_area_, -1, -1);
void GTKWindow::OnDestroy() { super::OnDestroy(); }
void GTKWindow::OnClose() {
if (!closing_ && window_) {
closing_ = true;
// After setting up the initial layout for non-fullscreen, enter fullscreen if
// requested.
if (IsFullscreen()) {
if (main_menu_widget) {
gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget);
}
gtk_window_fullscreen(GTK_WINDOW(window_));
}
super::OnClose();
}
bool GTKWindow::set_title(const std::string_view title) {
if (!super::set_title(title)) {
return false;
// Make sure the initial state after opening is reported to the common Window
// class no matter how GTK sends the events.
{
WindowDestructionReceiver destruction_receiver(this);
// TODO(Triang3l): Report the desired client area size.
GtkAllocation drawing_area_allocation;
gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation);
OnActualSizeUpdate(uint32_t(drawing_area_allocation.width),
uint32_t(drawing_area_allocation.height),
destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return true;
}
if (gtk_window_has_toplevel_focus(GTK_WINDOW(window_))) {
OnFocusUpdate(true, destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return true;
}
}
}
std::string titlez(title);
gtk_window_set_title(GTK_WINDOW(window_), (gchar*)titlez.c_str());
return true;
}
bool GTKWindow::SetIcon(const void* buffer, size_t size) {
// TODO(dougvj) Set icon after changin buffer to the correct format. (the
// call is gtk_window_set_icon)
return false;
}
void GTKWindow::RequestCloseImpl() { gtk_window_close(GTK_WINDOW(window_)); }
bool GTKWindow::is_fullscreen() const { return fullscreen_; }
void GTKWindow::ApplyNewFullscreen() {
// Various functions here may trigger events that may result in the listeners
// being invoked, and potentially cause the destruction of the window or
// fullscreen being toggled from inside this function.
WindowDestructionReceiver destruction_receiver(this);
void GTKWindow::ToggleFullscreen(bool fullscreen) {
if (fullscreen == is_fullscreen()) {
return;
}
const GTKMenuItem* main_menu = static_cast<const GTKMenuItem*>(GetMainMenu());
GtkWidget* main_menu_widget = main_menu ? main_menu->handle() : nullptr;
fullscreen_ = fullscreen;
if (fullscreen) {
// Changing the menu and the fullscreen state may change the size of the
// drawing area too, don't handle the resize multiple times (also potentially
// with the listeners changing the desired fullscreen if called from the
// handling of some event like GDK_CONFIGURE).
BeginBatchedSizeUpdate();
if (IsFullscreen()) {
if (main_menu_widget) {
gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
}
gtk_window_fullscreen(GTK_WINDOW(window_));
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
} else {
gtk_window_unfullscreen(GTK_WINDOW(window_));
}
}
bool GTKWindow::is_bordered() const {
return gtk_window_get_decorated(GTK_WINDOW(window_));
}
void GTKWindow::set_bordered(bool enabled) {
if (is_fullscreen()) {
// Don't screw with the borders if we're fullscreen.
return;
}
gtk_window_set_decorated(GTK_WINDOW(window_), enabled);
}
void GTKWindow::set_cursor_visible(bool value) {
if (is_cursor_visible_ == value) {
return;
}
if (value) {
// TODO(dougvj) Show and hide cursor
} else {
}
}
void GTKWindow::set_focus(bool value) {
if (has_focus_ == value) {
return;
}
if (window_) {
if (value) {
gtk_window_activate_focus(GTK_WINDOW(window_));
} else {
// TODO(dougvj) Check to see if we need to do something here to unset
// the focus.
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
} else {
has_focus_ = value;
}
}
void GTKWindow::Resize(int32_t width, int32_t height) {
if (is_fullscreen()) {
// Cannot resize while in fullscreen.
return;
}
gtk_window_resize(GTK_WINDOW(window_), width, height);
super::Resize(width, height);
}
void GTKWindow::Resize(int32_t left, int32_t top, int32_t right,
int32_t bottom) {
if (is_fullscreen()) {
// Cannot resize while in fullscreen.
return;
}
gtk_window_move(GTK_WINDOW(window_), left, top);
gtk_window_resize(GTK_WINDOW(window_), right - left, bottom - top);
super::Resize(left, top, right, bottom);
}
void GTKWindow::OnResize(UIEvent* e) { super::OnResize(e); }
void GTKWindow::Invalidate() {
// gtk_widget_queue_draw(drawing_area_);
super::Invalidate();
}
void GTKWindow::Close() {
if (closing_) {
return;
}
closing_ = true;
OnClose();
gtk_widget_destroy(window_);
window_ = nullptr;
}
void GTKWindow::OnMainMenuChange() {
// We need to store the old handle for detachment
static int count = 0;
auto main_menu = reinterpret_cast<GTKMenuItem*>(main_menu_.get());
if (main_menu && main_menu->handle()) {
if (!is_fullscreen()) {
gtk_box_pack_start(GTK_BOX(box_), main_menu->handle(), FALSE, FALSE, 0);
gtk_widget_show_all(window_);
} else {
gtk_container_remove(GTK_CONTAINER(box_), main_menu->handle());
if (main_menu_widget) {
gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, FALSE, FALSE, 0);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
// If the new menu is used for the first time, it will be in the hidden
// state initially. The menu might have been changed while in fullscreen
// without having shown it.
gtk_widget_show_all(main_menu_widget);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
}
}
}
bool GTKWindow::HandleWindowOwnerChange(GdkEventOwnerChange* event) {
if (event->type == GDK_OWNER_CHANGE) {
if (event->reason == GDK_OWNER_CHANGE_DESTROY) {
OnDestroy();
} else if (event->reason == GDK_OWNER_CHANGE_CLOSE) {
closing_ = true;
Close();
OnClose();
}
return true;
EndBatchedSizeUpdate(destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return;
}
return false;
}
bool GTKWindow::HandleWindowPaint() {
auto e = UIEvent(this);
OnPaint(&e);
return true;
void GTKWindow::ApplyNewTitle() {
gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str());
}
bool GTKWindow::HandleWindowResize(GdkEventConfigure* event) {
if (event->type == GDK_CONFIGURE) {
int32_t width = event->width;
int32_t height = event->height;
auto e = UIEvent(this);
if (width != width_ || height != height_) {
width_ = width;
height_ = height;
Layout();
}
OnResize(&e);
return true;
void GTKWindow::ApplyNewMainMenu(MenuItem* old_main_menu) {
if (IsFullscreen()) {
// The menu will be set when exiting fullscreen.
return;
}
return false;
}
// The fullscreen state may have been changed by some callback invoked, such
// as the configure (resize) one, recheck it after making changes also.
bool GTKWindow::HandleWindowVisibility(GdkEventVisibility* event) {
// TODO(dougvj) The gdk docs say that this is deprecated because modern window
// managers composite everything and nothing is truly hidden.
if (event->type == GDK_VISIBILITY_NOTIFY) {
if (event->state == GDK_VISIBILITY_UNOBSCURED) {
auto e = UIEvent(this);
OnVisible(&e);
} else {
auto e = UIEvent(this);
OnHidden(&e);
WindowDestructionReceiver destruction_receiver(this);
// Changing the menu may change the size of the drawing area too, and here the
// menu may be changed twice (to detach the old one and to attach the new),
// don't handle the resize multiple times.
BeginBatchedSizeUpdate();
if (old_main_menu) {
const GTKMenuItem& old_gtk_main_menu =
*static_cast<const GTKMenuItem*>(old_main_menu);
gtk_container_remove(GTK_CONTAINER(box_), old_gtk_main_menu.handle());
if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
return true;
}
return false;
}
bool GTKWindow::HandleWindowFocus(GdkEventFocus* event) {
if (event->type == GDK_FOCUS_CHANGE) {
if (!event->in) {
has_focus_ = false;
auto e = UIEvent(this);
OnLostFocus(&e);
} else {
has_focus_ = true;
auto e = UIEvent(this);
OnGotFocus(&e);
const GTKMenuItem* new_main_menu =
static_cast<const GTKMenuItem*>(GetMainMenu());
if (!new_main_menu) {
EndBatchedSizeUpdate(destruction_receiver);
return;
}
GtkWidget* new_main_menu_widget = new_main_menu->handle();
gtk_box_pack_start(GTK_BOX(box_), new_main_menu_widget, FALSE, FALSE, 0);
if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return true;
return;
}
// If the new menu is used for the first time, it will be in the hidden state
// initially.
gtk_widget_show_all(new_main_menu_widget);
if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) {
if (!destruction_receiver.IsWindowDestroyed()) {
EndBatchedSizeUpdate(destruction_receiver);
}
return;
}
EndBatchedSizeUpdate(destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return;
}
return false;
}
bool GTKWindow::HandleMouse(GdkEventAny* event) {
void GTKWindow::FocusImpl() { gtk_window_activate_focus(GTK_WINDOW(window_)); }
std::unique_ptr<Surface> GTKWindow::CreateSurfaceImpl(
Surface::TypeFlags allowed_types) {
GdkDisplay* display = gtk_widget_get_display(window_);
GdkWindow* drawing_area_window = gtk_widget_get_window(drawing_area_);
bool type_known = false;
bool type_supported_by_display = false;
if (allowed_types & Surface::kTypeFlag_XcbWindow) {
type_known = true;
if (GDK_IS_X11_DISPLAY(display)) {
type_supported_by_display = true;
return std::make_unique<XcbWindowSurface>(
XGetXCBConnection(gdk_x11_display_get_xdisplay(display)),
gdk_x11_window_get_xid(drawing_area_window));
}
}
// TODO(Triang3l): Wayland surface.
if (type_known && !type_supported_by_display) {
XELOGE(
"GTKWindow: The window system of the GTK window is not supported by "
"Xenia");
}
return nullptr;
}
void GTKWindow::RequestPaintImpl() { gtk_widget_queue_draw(drawing_area_); }
void GTKWindow::HandleSizeUpdate(
WindowDestructionReceiver& destruction_receiver) {
if (!drawing_area_) {
// Batched size update ended when the window has already been closed, for
// instance.
return;
}
// TODO(Triang3l): Report the desired client area size.
GtkAllocation drawing_area_allocation;
gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation);
OnActualSizeUpdate(uint32_t(drawing_area_allocation.width),
uint32_t(drawing_area_allocation.height),
destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return;
}
}
void GTKWindow::BeginBatchedSizeUpdate() {
// It's okay if batched_size_update_contained_* are not false when beginning
// a batched update, in case the new batched update was started by a window
// listener called from within EndBatchedSizeUpdate.
++batched_size_update_depth_;
}
void GTKWindow::EndBatchedSizeUpdate(
WindowDestructionReceiver& destruction_receiver) {
assert_not_zero(batched_size_update_depth_);
if (--batched_size_update_depth_) {
return;
}
// Resetting batched_size_update_contained_* in closing, not opening, because
// a listener may start a new batch, and finish it, and there won't be need to
// handle the deferred messages twice.
if (batched_size_update_contained_configure_) {
batched_size_update_contained_configure_ = false;
HandleSizeUpdate(destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
return;
}
}
if (batched_size_update_contained_draw_) {
batched_size_update_contained_draw_ = false;
RequestPaint();
}
}
bool GTKWindow::HandleMouse(GdkEvent* event,
WindowDestructionReceiver& destruction_receiver) {
MouseEvent::Button button = MouseEvent::Button::kNone;
int32_t dx = 0;
int32_t dy = 0;
int32_t x = 0;
int32_t y = 0;
int32_t scroll_x = 0;
int32_t scroll_y = 0;
switch (event->type) {
default:
// Double click/etc?
return true;
case GDK_MOTION_NOTIFY: {
auto motion_event = reinterpret_cast<const GdkEventMotion*>(event);
x = motion_event->x;
y = motion_event->y;
} break;
case GDK_BUTTON_PRESS:
case GDK_BUTTON_RELEASE: {
GdkEventButton* e = reinterpret_cast<GdkEventButton*>(event);
switch (e->button) {
auto button_event = reinterpret_cast<const GdkEventButton*>(event);
switch (button_event->button) {
case 1:
button = MouseEvent::Button::kLeft;
break;
case 3:
button = MouseEvent::Button::kRight;
break;
case 2:
button = MouseEvent::Button::kMiddle;
break;
case 3:
button = MouseEvent::Button::kRight;
break;
case 4:
button = MouseEvent::Button::kX1;
break;
case 5:
button = MouseEvent::Button::kX2;
break;
default:
// Still handle the movement.
break;
}
x = e->x;
y = e->y;
break;
}
case GDK_MOTION_NOTIFY: {
GdkEventMotion* e = reinterpret_cast<GdkEventMotion*>(event);
x = e->x;
y = e->y;
break;
}
x = button_event->x;
y = button_event->y;
} break;
case GDK_SCROLL: {
GdkEventScroll* e = reinterpret_cast<GdkEventScroll*>(event);
x = e->x;
y = e->y;
dx = e->delta_x;
dy = e->delta_y;
break;
}
}
auto e = MouseEvent(this, button, x, y, dx, dy);
switch (event->type) {
case GDK_BUTTON_PRESS:
OnMouseDown(&e);
break;
case GDK_BUTTON_RELEASE:
OnMouseUp(&e);
break;
case GDK_MOTION_NOTIFY:
OnMouseMove(&e);
break;
case GDK_SCROLL:
OnMouseWheel(&e);
break;
auto scroll_event = reinterpret_cast<const GdkEventScroll*>(event);
x = scroll_event->x;
y = scroll_event->y;
scroll_x = scroll_event->delta_x * MouseEvent::kScrollPerDetent;
// In GDK, positive is towards the bottom of the screen, not forward from
// the user.
scroll_y = -scroll_event->delta_y * MouseEvent::kScrollPerDetent;
} break;
default:
return false;
}
MouseEvent e(this, button, x, y, scroll_x, scroll_y);
switch (event->type) {
case GDK_MOTION_NOTIFY:
OnMouseMove(e, destruction_receiver);
break;
case GDK_BUTTON_PRESS:
OnMouseDown(e, destruction_receiver);
break;
case GDK_BUTTON_RELEASE:
OnMouseUp(e, destruction_receiver);
break;
case GDK_SCROLL:
OnMouseWheel(e, destruction_receiver);
break;
default:
break;
}
// Returning immediately anyway - no need to check
// destruction_receiver.IsWindowDestroyed().
return e.is_handled();
}
bool GTKWindow::HandleKeyboard(GdkEventKey* event) {
bool GTKWindow::HandleKeyboard(
GdkEventKey* event, WindowDestructionReceiver& destruction_receiver) {
unsigned int modifiers = event->state;
bool shift_pressed = modifiers & GDK_SHIFT_MASK;
bool ctrl_pressed = modifiers & GDK_CONTROL_MASK;
@@ -408,25 +433,154 @@ bool GTKWindow::HandleKeyboard(GdkEventKey* event) {
bool super_pressed = modifiers & GDK_SUPER_MASK;
uint32_t key_char = gdk_keyval_to_unicode(event->keyval);
// TODO(Triang3l): event->hardware_keycode to VirtualKey translation.
auto e = KeyEvent(this, VirtualKey(event->hardware_keycode), 1,
event->type == GDK_KEY_RELEASE, shift_pressed, ctrl_pressed,
alt_pressed, super_pressed);
KeyEvent e(this, VirtualKey(event->hardware_keycode), 1,
event->type == GDK_KEY_RELEASE, shift_pressed, ctrl_pressed,
alt_pressed, super_pressed);
switch (event->type) {
case GDK_KEY_PRESS:
OnKeyDown(&e);
OnKeyDown(e, destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
return e.is_handled();
}
if (key_char > 0) {
OnKeyChar(&e);
OnKeyChar(e, destruction_receiver);
}
break;
case GDK_KEY_RELEASE:
OnKeyUp(&e);
OnKeyUp(e, destruction_receiver);
break;
default:
return false;
break;
}
// Returning immediately anyway - no need to check
// destruction_receiver.IsWindowDestroyed().
return e.is_handled();
}
gboolean GTKWindow::WindowEventHandler(GdkEvent* event) {
switch (event->type) {
case GDK_DELETE:
// In case the widget was somehow forcibly destroyed without GDK_DELETE.
case GDK_DESTROY: {
WindowDestructionReceiver destruction_receiver(this);
OnBeforeClose(destruction_receiver);
if (destruction_receiver.IsWindowDestroyed()) {
break;
}
// Set window_ to null to ignore events from now on since this
// ui::GTKWindow is entering an indeterminate state - this should be done
// at some point in closing anyway.
GtkWidget* window = window_;
window_ = nullptr;
// Destroying the top-level window also destroys its children.
drawing_area_ = nullptr;
box_ = nullptr;
if (event->type != GDK_DESTROY) {
gtk_widget_destroy(window);
}
OnAfterClose();
} break;
case GDK_FOCUS_CHANGE: {
auto focus_event = reinterpret_cast<const GdkEventFocus*>(event);
WindowDestructionReceiver destruction_receiver(this);
OnFocusUpdate(bool(focus_event->in), destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
break;
}
} break;
case GDK_KEY_PRESS:
case GDK_KEY_RELEASE: {
WindowDestructionReceiver destruction_receiver(this);
HandleKeyboard(reinterpret_cast<GdkEventKey*>(event),
destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
break;
}
} break;
default:
break;
}
// The window might have been destroyed by the handlers, don't interact with
// *this in this function from now on.
return GDK_EVENT_PROPAGATE;
}
gboolean GTKWindow::WindowEventHandlerThunk(GtkWidget* widget, GdkEvent* event,
gpointer user_data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(user_data);
if (!window || widget != window->window_ ||
reinterpret_cast<const GdkEventAny*>(event)->window !=
gtk_widget_get_window(window->window_)) {
return GDK_EVENT_PROPAGATE;
}
return window->WindowEventHandler(event);
}
gboolean GTKWindow::DrawingAreaEventHandler(GdkEvent* event) {
switch (event->type) {
case GDK_MOTION_NOTIFY:
case GDK_BUTTON_PRESS:
case GDK_BUTTON_RELEASE:
case GDK_SCROLL: {
WindowDestructionReceiver destruction_receiver(this);
HandleMouse(event, destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
break;
}
} break;
case GDK_CONFIGURE: {
if (batched_size_update_depth_) {
batched_size_update_contained_configure_ = true;
} else {
WindowDestructionReceiver destruction_receiver(this);
HandleSizeUpdate(destruction_receiver);
if (destruction_receiver.IsWindowDestroyedOrClosed()) {
break;
}
}
} break;
default:
break;
}
// The window might have been destroyed by the handlers, don't interact with
// *this in this function from now on.
return GDK_EVENT_PROPAGATE;
}
gboolean GTKWindow::DrawingAreaEventHandlerThunk(GtkWidget* widget,
GdkEvent* event,
gpointer user_data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(user_data);
if (!window || widget != window->drawing_area_ ||
reinterpret_cast<const GdkEventAny*>(event)->window !=
gtk_widget_get_window(window->drawing_area_)) {
return GDK_EVENT_PROPAGATE;
}
return window->DrawingAreaEventHandler(event);
}
gboolean GTKWindow::DrawHandler(GtkWidget* widget, cairo_t* cr,
gpointer user_data) {
GTKWindow* window = reinterpret_cast<GTKWindow*>(user_data);
if (!window || widget != window->drawing_area_) {
return FALSE;
}
if (window->batched_size_update_depth_) {
window->batched_size_update_contained_draw_ = true;
} else {
window->OnPaint();
}
return TRUE;
}
std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,
const std::string& text,
const std::string& hotkey,
@@ -434,17 +588,10 @@ std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,
return std::make_unique<GTKMenuItem>(type, text, hotkey, callback);
}
static void _menu_activate_callback(GtkWidget* gtk_menu, gpointer data) {
GTKMenuItem* menu = reinterpret_cast<GTKMenuItem*>(data);
menu->Activate();
}
void GTKMenuItem::Activate() {
try {
callback_();
} catch (const std::bad_function_call& e) {
// Ignore
}
void GTKMenuItem::ActivateHandler(GtkWidget* menu_item, gpointer user_data) {
static_cast<GTKMenuItem*>(user_data)->OnSelected();
// The menu item might have been destroyed by its OnSelected, don't do
// anything with it here from now on.
}
GTKMenuItem::GTKMenuItem(Type type, const std::string& text,
@@ -475,13 +622,20 @@ GTKMenuItem::GTKMenuItem(Type type, const std::string& text,
menu_ = gtk_menu_item_new_with_mnemonic(gtk_label);
break;
}
if (GTK_IS_MENU_ITEM(menu_))
g_signal_connect(menu_, "activate", G_CALLBACK(_menu_activate_callback),
(gpointer)this);
if (menu_) {
// Own the object because it may be detached from and re-attached to a
// Window.
g_object_ref_sink(menu_);
if (GTK_IS_MENU_ITEM(menu_)) {
g_signal_connect(menu_, "activate", G_CALLBACK(ActivateHandler),
reinterpret_cast<gpointer>(this));
}
}
}
GTKMenuItem::~GTKMenuItem() {
if (menu_) {
g_object_unref(menu_);
}
}