UI: Improve GTK GUI Abstractions for Linux
This commit is contained in:
parent
10e5425155
commit
d92cd9f6a1
|
@ -14,15 +14,6 @@
|
|||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class PostedFn {
|
||||
public:
|
||||
explicit PostedFn(std::function<void()> fn) : fn_(std::move(fn)) {}
|
||||
void Call() { fn_(); }
|
||||
|
||||
private:
|
||||
std::function<void()> fn_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Loop> Loop::Create() { return std::make_unique<GTKLoop>(); }
|
||||
|
||||
GTKLoop::GTKLoop() : thread_id_() {
|
||||
|
@ -53,24 +44,33 @@ bool GTKLoop::is_on_loop_thread() {
|
|||
}
|
||||
|
||||
gboolean _posted_fn_thunk(gpointer posted_fn) {
|
||||
PostedFn* Fn = reinterpret_cast<PostedFn*>(posted_fn);
|
||||
Fn->Call();
|
||||
// convert gpointer back to std::function, call it, then free std::function
|
||||
std::function<void()>* f =
|
||||
reinterpret_cast<std::function<void()>*>(posted_fn);
|
||||
std::function<void()>& func = *f;
|
||||
func();
|
||||
delete f;
|
||||
// Tells GDK we don't want to run this again
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void GTKLoop::Post(std::function<void()> fn) {
|
||||
assert_true(thread_id_ != std::thread::id());
|
||||
gdk_threads_add_idle(_posted_fn_thunk,
|
||||
reinterpret_cast<gpointer>(new PostedFn(std::move(fn))));
|
||||
// converting std::function to a generic pointer for gdk
|
||||
gdk_threads_add_idle(_posted_fn_thunk, reinterpret_cast<gpointer>(
|
||||
new std::function<void()>(fn)));
|
||||
}
|
||||
|
||||
void GTKLoop::PostDelayed(std::function<void()> fn, uint64_t delay_millis) {
|
||||
gdk_threads_add_timeout(
|
||||
delay_millis, _posted_fn_thunk,
|
||||
reinterpret_cast<gpointer>(new PostedFn(std::move(fn))));
|
||||
reinterpret_cast<gpointer>(new std::function<void()>(fn)));
|
||||
}
|
||||
|
||||
void GTKLoop::Quit() { assert_true(thread_id_ != std::thread::id()); }
|
||||
void GTKLoop::Quit() {
|
||||
assert_true(thread_id_ != std::thread::id());
|
||||
Post([]() { gtk_main_quit(); });
|
||||
}
|
||||
|
||||
void GTKLoop::AwaitQuit() { quit_fence_.Wait(); }
|
||||
|
||||
|
|
|
@ -169,8 +169,9 @@ class Window {
|
|||
Loop* loop_ = nullptr;
|
||||
std::unique_ptr<MenuItem> main_menu_;
|
||||
std::wstring title_;
|
||||
int32_t width_ = 0;
|
||||
int32_t height_ = 0;
|
||||
// GTK must have a default value here that isn't 0
|
||||
int32_t width_ = 1280;
|
||||
int32_t height_ = 720;
|
||||
bool has_focus_ = true;
|
||||
bool is_cursor_visible_ = true;
|
||||
bool is_imgui_input_enabled_ = false;
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "xenia/base/assert.h"
|
||||
#include "xenia/base/clock.h"
|
||||
#include "xenia/base/logging.h"
|
||||
#include "xenia/base/platform_linux.h"
|
||||
#include "xenia/ui/window_gtk.h"
|
||||
|
@ -17,15 +19,6 @@
|
|||
namespace xe {
|
||||
namespace ui {
|
||||
|
||||
class FnWrapper {
|
||||
public:
|
||||
explicit FnWrapper(std::function<void()> fn) : fn_(std::move(fn)) {}
|
||||
void Call() { fn_(); }
|
||||
|
||||
private:
|
||||
std::function<void()> fn_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Window> Window::Create(Loop* loop, const std::wstring& title) {
|
||||
return std::make_unique<GTKWindow>(loop, title);
|
||||
}
|
||||
|
@ -36,14 +29,14 @@ GTKWindow::GTKWindow(Loop* loop, const std::wstring& title)
|
|||
GTKWindow::~GTKWindow() {
|
||||
OnDestroy();
|
||||
if (window_) {
|
||||
gtk_widget_destroy(window_);
|
||||
if (GTK_IS_WIDGET(window_)) gtk_widget_destroy(window_);
|
||||
window_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool GTKWindow::Initialize() { return OnCreate(); }
|
||||
|
||||
void gtk_event_handler_(GtkWidget* widget, GdkEvent* event, gpointer data) {
|
||||
gboolean gtk_event_handler(GtkWidget* widget, GdkEvent* event, gpointer data) {
|
||||
GTKWindow* window = reinterpret_cast<GTKWindow*>(data);
|
||||
switch (event->type) {
|
||||
case GDK_OWNER_CHANGE:
|
||||
|
@ -57,34 +50,81 @@ void gtk_event_handler_(GtkWidget* widget, GdkEvent* event, gpointer data) {
|
|||
window->HandleKeyboard(&(event->key));
|
||||
break;
|
||||
case GDK_SCROLL:
|
||||
case GDK_BUTTON_PRESS:
|
||||
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:
|
||||
window->HandleWindowResize(&(event->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
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
void GTKWindow::Create() {
|
||||
// GTK optionally allows passing argv and argc here for parsing gtk specific
|
||||
// options. We won't bother
|
||||
window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);
|
||||
gtk_window_set_title(GTK_WINDOW(window_), (gchar*)title_.c_str());
|
||||
gtk_window_set_default_size(GTK_WINDOW(window_), 1280, 720);
|
||||
gtk_widget_show_all(window_);
|
||||
g_signal_connect(G_OBJECT(window_), "destroy", G_CALLBACK(gtk_main_quit),
|
||||
NULL);
|
||||
g_signal_connect(G_OBJECT(window_), "event", G_CALLBACK(gtk_event_handler_),
|
||||
gtk_window_set_resizable(GTK_WINDOW(window_), true);
|
||||
gtk_window_set_title(GTK_WINDOW(window_), xe::to_string(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();
|
||||
// Don't allow resizing the window below this
|
||||
gtk_widget_set_size_request(drawing_area_, 640, 480);
|
||||
// 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));
|
||||
|
||||
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
|
||||
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_);
|
||||
gtk_widget_show_all(window_);
|
||||
}
|
||||
|
||||
bool GTKWindow::OnCreate() {
|
||||
|
@ -107,7 +147,7 @@ bool GTKWindow::set_title(const std::wstring& title) {
|
|||
if (!super::set_title(title)) {
|
||||
return false;
|
||||
}
|
||||
gtk_window_set_title(GTK_WINDOW(window_), (gchar*)title.c_str());
|
||||
gtk_window_set_title(GTK_WINDOW(window_), xe::to_string(title).c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -125,7 +165,6 @@ void GTKWindow::ToggleFullscreen(bool fullscreen) {
|
|||
}
|
||||
|
||||
fullscreen_ = fullscreen;
|
||||
|
||||
if (fullscreen) {
|
||||
gtk_window_fullscreen(GTK_WINDOW(window_));
|
||||
} else {
|
||||
|
@ -142,7 +181,6 @@ void GTKWindow::set_bordered(bool enabled) {
|
|||
// Don't screw with the borders if we're fullscreen.
|
||||
return;
|
||||
}
|
||||
|
||||
gtk_window_set_decorated(GTK_WINDOW(window_), enabled);
|
||||
}
|
||||
|
||||
|
@ -164,7 +202,7 @@ void GTKWindow::set_focus(bool value) {
|
|||
if (value) {
|
||||
gtk_window_activate_focus(GTK_WINDOW(window_));
|
||||
} else {
|
||||
// TODO(dougvj) Check to see if we need to do something here to unset
|
||||
// TODO(dougvj) Check to see if we need to do somethign here to unset
|
||||
// the focus.
|
||||
}
|
||||
} else {
|
||||
|
@ -173,31 +211,26 @@ void GTKWindow::set_focus(bool value) {
|
|||
}
|
||||
|
||||
void GTKWindow::Resize(int32_t width, int32_t height) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
gtk_window_resize(GTK_WINDOW(window_), width, height);
|
||||
}
|
||||
|
||||
void GTKWindow::Resize(int32_t left, int32_t top, int32_t right,
|
||||
int32_t bottom) {
|
||||
// TODO(dougvj) Verify that this is the desired behavior from this call
|
||||
int32_t width = right - left;
|
||||
int32_t height = bottom - top;
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
gtk_window_move(GTK_WINDOW(window_), left, top);
|
||||
gtk_window_resize(GTK_WINDOW(window_), left - right, top - bottom);
|
||||
gtk_window_resize(GTK_WINDOW(window_), width, height);
|
||||
}
|
||||
|
||||
void GTKWindow::OnResize(UIEvent* e) {
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
gtk_window_get_size(GTK_WINDOW(window_), &width, &height);
|
||||
if (width != width_ || height != height_) {
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
Layout();
|
||||
}
|
||||
super::OnResize(e);
|
||||
}
|
||||
void GTKWindow::OnResize(UIEvent* e) { super::OnResize(e); }
|
||||
|
||||
void GTKWindow::Invalidate() {
|
||||
// gtk_widget_queue_draw(drawing_area_);
|
||||
super::Invalidate();
|
||||
// TODO(dougvj) I am not sure what this is supposed to do
|
||||
}
|
||||
|
||||
void GTKWindow::Close() {
|
||||
|
@ -211,14 +244,15 @@ void GTKWindow::Close() {
|
|||
|
||||
void GTKWindow::OnMainMenuChange() {
|
||||
// We need to store the old handle for detachment
|
||||
static GtkWidget* box = nullptr;
|
||||
static int count = 0;
|
||||
auto main_menu = reinterpret_cast<GTKMenuItem*>(main_menu_.get());
|
||||
if (main_menu && !is_fullscreen()) {
|
||||
if (box) gtk_widget_destroy(box);
|
||||
GtkWidget* menu = main_menu->handle();
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
|
||||
gtk_box_pack_start(GTK_BOX(box), menu, FALSE, FALSE, 3);
|
||||
gtk_container_add(GTK_CONTAINER(window_), box);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,9 +270,22 @@ bool GTKWindow::HandleWindowOwnerChange(GdkEventOwnerChange* event) {
|
|||
return false;
|
||||
}
|
||||
|
||||
bool GTKWindow::HandleWindowPaint() {
|
||||
auto e = UIEvent(this);
|
||||
OnPaint(&e);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -353,18 +400,18 @@ bool GTKWindow::HandleKeyboard(GdkEventKey* event) {
|
|||
bool ctrl_pressed = modifiers & GDK_CONTROL_MASK;
|
||||
bool alt_pressed = modifiers & GDK_META_MASK;
|
||||
bool super_pressed = modifiers & GDK_SUPER_MASK;
|
||||
uint32_t key_char = gdk_keyval_to_unicode(event->keyval);
|
||||
auto e =
|
||||
KeyEvent(this, 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);
|
||||
if (key_char > 0) OnKeyChar(&e);
|
||||
break;
|
||||
case GDK_KEY_RELEASE:
|
||||
OnKeyUp(&e);
|
||||
break;
|
||||
// TODO(dougvj) GDK doesn't have a KEY CHAR event, so we will have to
|
||||
// figure out its equivalent here to call OnKeyChar(&e);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
@ -378,23 +425,35 @@ std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,
|
|||
return std::make_unique<GTKMenuItem>(type, text, hotkey, callback);
|
||||
}
|
||||
|
||||
static void _menu_activate_callback(GtkWidget* menu, gpointer data) {
|
||||
auto fn = reinterpret_cast<FnWrapper*>(data);
|
||||
fn->Call();
|
||||
delete fn;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
GTKMenuItem::GTKMenuItem(Type type, const std::wstring& text,
|
||||
const std::wstring& hotkey,
|
||||
std::function<void()> callback)
|
||||
: MenuItem(type, text, hotkey, std::move(callback)) {
|
||||
std::string label = xe::to_string(text);
|
||||
// TODO(dougvj) Would we ever need to escape underscores?
|
||||
// Replace & with _ for gtk to see the memonic
|
||||
std::replace(label.begin(), label.end(), '&', '_');
|
||||
const gchar* gtk_label = reinterpret_cast<const gchar*>(label.c_str());
|
||||
switch (type) {
|
||||
case MenuItem::Type::kNormal:
|
||||
default:
|
||||
menu_ = gtk_menu_bar_new();
|
||||
break;
|
||||
case MenuItem::Type::kPopup:
|
||||
menu_ = gtk_menu_item_new_with_label((gchar*)xe::to_string(text).c_str());
|
||||
menu_ = gtk_menu_item_new_with_mnemonic(gtk_label);
|
||||
break;
|
||||
case MenuItem::Type::kSeparator:
|
||||
menu_ = gtk_separator_menu_item_new();
|
||||
|
@ -402,15 +461,14 @@ GTKMenuItem::GTKMenuItem(Type type, const std::wstring& text,
|
|||
case MenuItem::Type::kString:
|
||||
auto full_name = text;
|
||||
if (!hotkey.empty()) {
|
||||
full_name += L"\t" + hotkey;
|
||||
full_name += L" " + hotkey;
|
||||
}
|
||||
menu_ = gtk_menu_item_new_with_label(
|
||||
(gchar*)xe::to_string(full_name).c_str());
|
||||
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) new FnWrapper(callback));
|
||||
(gpointer)this);
|
||||
}
|
||||
|
||||
GTKMenuItem::~GTKMenuItem() {
|
||||
|
@ -452,4 +510,5 @@ void GTKMenuItem::OnChildRemoved(MenuItem* generic_child_item) {
|
|||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
} // namespace xe
|
||||
|
|
|
@ -30,7 +30,7 @@ class GTKWindow : public Window {
|
|||
NativePlatformHandle native_platform_handle() const override {
|
||||
return connection_;
|
||||
}
|
||||
NativeWindowHandle native_handle() const override { return window_; }
|
||||
NativeWindowHandle native_handle() const override { return drawing_area_; }
|
||||
|
||||
void EnableMainMenu() override {}
|
||||
void DisableMainMenu() override {}
|
||||
|
@ -67,15 +67,22 @@ class GTKWindow : public Window {
|
|||
private:
|
||||
void Create();
|
||||
GtkWidget* window_;
|
||||
GtkWidget* box_;
|
||||
GtkWidget* drawing_area_;
|
||||
xcb_connection_t* connection_;
|
||||
|
||||
friend void gtk_event_handler_(GtkWidget*, GdkEvent*, gpointer);
|
||||
// C Callback shims for GTK
|
||||
friend gboolean gtk_event_handler(GtkWidget*, GdkEvent*, gpointer);
|
||||
friend gboolean close_callback(GtkWidget*, gpointer);
|
||||
friend gboolean draw_callback(GtkWidget*, GdkFrameClock*, gpointer);
|
||||
|
||||
bool HandleMouse(GdkEventAny* event);
|
||||
bool HandleKeyboard(GdkEventKey* event);
|
||||
bool HandleWindowResize(GdkEventConfigure* event);
|
||||
bool HandleWindowFocus(GdkEventFocus* event);
|
||||
bool HandleWindowVisibility(GdkEventVisibility* event);
|
||||
bool HandleWindowOwnerChange(GdkEventOwnerChange* event);
|
||||
bool HandleWindowPaint();
|
||||
|
||||
bool closing_ = false;
|
||||
bool fullscreen_ = false;
|
||||
|
@ -89,6 +96,7 @@ class GTKMenuItem : public MenuItem {
|
|||
|
||||
GtkWidget* handle() { return menu_; }
|
||||
using MenuItem::OnSelected;
|
||||
void Activate();
|
||||
|
||||
void EnableMenuItem(Window& window) override {}
|
||||
void DisableMenuItem(Window& window) override {}
|
||||
|
|
Loading…
Reference in New Issue