basalt-gtk 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runLinux.js +29 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +362 -0
- package/native/gtk/ColorSchemeGtk.cpp +58 -0
- package/native/gtk/ComponentRegistryGtk.cpp +101 -0
- package/native/gtk/FontRegistryFontconfig.cpp +127 -0
- package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
- package/native/gtk/GtkAnimationChoreographer.h +49 -0
- package/native/gtk/GtkDropTarget.cpp +269 -0
- package/native/gtk/GtkDropTarget.h +40 -0
- package/native/gtk/GtkFocus.cpp +296 -0
- package/native/gtk/GtkFocus.h +113 -0
- package/native/gtk/GtkImageLoader.cpp +128 -0
- package/native/gtk/GtkImageLoader.h +87 -0
- package/native/gtk/GtkKeyEvents.cpp +91 -0
- package/native/gtk/GtkKeyEvents.h +21 -0
- package/native/gtk/GtkMenuBar.cpp +45 -0
- package/native/gtk/GtkMountingManager.cpp +1027 -0
- package/native/gtk/GtkMountingManager.h +146 -0
- package/native/gtk/GtkRunLoopObserver.cpp +77 -0
- package/native/gtk/GtkRunLoopObserver.h +41 -0
- package/native/gtk/GtkScrollView.cpp +626 -0
- package/native/gtk/GtkScrollView.h +209 -0
- package/native/gtk/GtkSkiaContext.cpp +248 -0
- package/native/gtk/GtkSkiaContext.h +62 -0
- package/native/gtk/GtkSkiaModule.cpp +80 -0
- package/native/gtk/GtkSkiaModule.h +51 -0
- package/native/gtk/GtkSkiaPeer.cpp +285 -0
- package/native/gtk/GtkSkiaPeer.h +63 -0
- package/native/gtk/GtkTextInput.cpp +428 -0
- package/native/gtk/GtkTextInput.h +123 -0
- package/native/gtk/GtkTextPeer.cpp +425 -0
- package/native/gtk/GtkTextPeer.h +87 -0
- package/native/gtk/GtkTitleBar.cpp +273 -0
- package/native/gtk/GtkTitleBar.h +96 -0
- package/native/gtk/GtkTitleBarLayout.cpp +45 -0
- package/native/gtk/GtkTitleBarLayout.h +36 -0
- package/native/gtk/GtkTouchDispatcher.cpp +671 -0
- package/native/gtk/GtkTouchDispatcher.h +139 -0
- package/native/gtk/GtkWindowControl.cpp +223 -0
- package/native/gtk/GtkWindowModule.cpp +194 -0
- package/native/gtk/GtkWindowModule.h +62 -0
- package/native/gtk/PangoTextLayout.cpp +295 -0
- package/native/gtk/PangoTextLayout.h +54 -0
- package/native/gtk/PangoTextLayoutManager.cpp +140 -0
- package/native/gtk/PlatformServicesGtk.cpp +583 -0
- package/native/gtk/RnView.cpp +1407 -0
- package/native/gtk/RnView.h +339 -0
- package/native/gtk/demo_layout_gtk.cpp +57 -0
- package/native/gtk/main_gtk.cpp +1981 -0
- package/native/gtk/mount_harness_gtk.cpp +143 -0
- package/native/tests/TestMainGtk.cpp +25 -0
- package/native/tests/notification_stub.cpp +103 -0
- package/native/tests/test_accessibility.cpp +251 -0
- package/native/tests/test_focus.cpp +194 -0
- package/native/tests/test_hittest.cpp +242 -0
- package/native/tests/test_image.cpp +135 -0
- package/native/tests/test_key_events_gtk.cpp +112 -0
- package/native/tests/test_mounting.cpp +415 -0
- package/native/tests/test_scrollview.cpp +258 -0
- package/native/tests/test_skia_texture.cpp +120 -0
- package/native/tests/test_text.cpp +147 -0
- package/native/tests/test_textinput.cpp +598 -0
- package/native/tests/test_titlebar.cpp +137 -0
- package/native/tests/test_titlebar_drag.cpp +138 -0
- package/native/tests/test_version.cpp +49 -0
- package/native/tests/test_view.cpp +162 -0
- package/native/tests/test_viewprops.cpp +254 -0
- package/package.json +26 -0
- package/react-native.config.js +21 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// GTK input into React Native's touch events.
|
|
2
|
+
//
|
|
3
|
+
// React Native's Pressability -- what backs <Pressable>, <TouchableOpacity> and
|
|
4
|
+
// every onPress in an app -- runs on the responder system in JavaScript, and
|
|
5
|
+
// the responder system is fed by touchstart/touchmove/touchend. So a pointer on
|
|
6
|
+
// a desktop is reported here as a single touch point, which is also what React
|
|
7
|
+
// Native for Windows and macOS do. W3C pointer events exist alongside these and
|
|
8
|
+
// are only consulted for hover, behind a feature flag.
|
|
9
|
+
//
|
|
10
|
+
// Controllers are attached to the surface root, not to every view. A GTK
|
|
11
|
+
// controller per widget would mean creating and destroying controllers on every
|
|
12
|
+
// mutation, and would still need the same hit test: `gtk_widget_pick` already
|
|
13
|
+
// walks the widget tree and returns the deepest widget at a point, which is the
|
|
14
|
+
// same answer React Native's own hit testing is looking for now that every view
|
|
15
|
+
// is allocated at the frame Yoga gave it.
|
|
16
|
+
|
|
17
|
+
#pragma once
|
|
18
|
+
|
|
19
|
+
#include "GtkMountingManager.h"
|
|
20
|
+
#include "HoverTracker.h"
|
|
21
|
+
#include "PointerButtons.h"
|
|
22
|
+
#include "RnView.h"
|
|
23
|
+
|
|
24
|
+
#include <react/renderer/components/view/TouchEventEmitter.h>
|
|
25
|
+
#include <react/renderer/graphics/Point.h>
|
|
26
|
+
|
|
27
|
+
namespace basalt {
|
|
28
|
+
|
|
29
|
+
// The tag of the React Native view under a point, in `root`'s coordinates, or 0
|
|
30
|
+
// if the point hits nothing.
|
|
31
|
+
//
|
|
32
|
+
// A free function because it is a pure function of the widget tree and nothing
|
|
33
|
+
// else, which is also what makes it testable: hit testing is the part of input
|
|
34
|
+
// most likely to be quietly wrong, and it needs no gestures to exercise.
|
|
35
|
+
facebook::react::Tag hitTestTag(RnView *root, double x, double y);
|
|
36
|
+
|
|
37
|
+
class GtkTouchDispatcher {
|
|
38
|
+
public:
|
|
39
|
+
GtkTouchDispatcher(GtkMountingManager *mountingManager, RnView *surfaceRoot);
|
|
40
|
+
~GtkTouchDispatcher();
|
|
41
|
+
|
|
42
|
+
GtkTouchDispatcher(const GtkTouchDispatcher &) = delete;
|
|
43
|
+
GtkTouchDispatcher &operator=(const GtkTouchDispatcher &) = delete;
|
|
44
|
+
GtkTouchDispatcher(GtkTouchDispatcher &&) = delete;
|
|
45
|
+
GtkTouchDispatcher &operator=(GtkTouchDispatcher &&) = delete;
|
|
46
|
+
|
|
47
|
+
// Synthesises a press and release at a point in surface-root coordinates,
|
|
48
|
+
// entering at the same place the GTK gesture callbacks do.
|
|
49
|
+
//
|
|
50
|
+
// This exists because the input path is otherwise untestable in automation:
|
|
51
|
+
// synthesising a real pointer event means driving the window system, which on
|
|
52
|
+
// macOS needs accessibility permission a headless run does not have. It skips
|
|
53
|
+
// GDK's event delivery and nothing else, so it proves hit testing, emitter
|
|
54
|
+
// lookup and event-beat delivery, but not that GTK routes clicks here.
|
|
55
|
+
void synthesiseTap(double x, double y);
|
|
56
|
+
// The same, with a button. BASALT_TEST_SECONDARY_TAP is what needs it: a
|
|
57
|
+
// right-click cannot be injected any other way, and it is the one click whose
|
|
58
|
+
// whole point is that it does *not* press what it lands on.
|
|
59
|
+
void synthesiseTap(double x, double y, basalt::PointerButton button);
|
|
60
|
+
|
|
61
|
+
// A press, a run of moves, and a release. The same reason as synthesiseTap,
|
|
62
|
+
// one step further: a gesture recogniser cannot be exercised by a tap at all
|
|
63
|
+
// -- a pan is defined by the movement between the two -- so a drag has to be
|
|
64
|
+
// injectable for anything about it to be provable outside a person's hand.
|
|
65
|
+
void synthesiseDrag(double fromX, double fromY, double toX, double toY, int steps);
|
|
66
|
+
|
|
67
|
+
// Moves the pointer without pressing it, which is what produces hover. Same
|
|
68
|
+
// reason as the two above, and the same entry point the motion controller
|
|
69
|
+
// uses. A negative coordinate means the pointer left the surface.
|
|
70
|
+
void synthesiseHover(double x, double y);
|
|
71
|
+
|
|
72
|
+
private:
|
|
73
|
+
static void onPressed(GtkGestureClick *gesture, int count, double x, double y, gpointer userData);
|
|
74
|
+
static void onReleased(GtkGestureClick *gesture, int count, double x, double y, gpointer userData);
|
|
75
|
+
static void onCancelled(GtkGesture *gesture, GdkEventSequence *sequence, gpointer userData);
|
|
76
|
+
static void onMotion(GtkEventControllerMotion *controller, double x, double y, gpointer userData);
|
|
77
|
+
static void onPointerLeft(GtkEventControllerMotion *controller, gpointer userData);
|
|
78
|
+
|
|
79
|
+
// `button` decides whether this presses anything. Only the primary one drives
|
|
80
|
+
// the touch model; a secondary or middle click produces a pointer event and
|
|
81
|
+
// nothing else, which is what keeps a right-click from firing every
|
|
82
|
+
// `<Pressable>` it lands on. See core/PointerButtons.h.
|
|
83
|
+
void dispatchTouchStart(double x, double y, basalt::PointerButton button);
|
|
84
|
+
void dispatchTouchMove(double x, double y);
|
|
85
|
+
void dispatchTouchEnd(double x, double y, basalt::PointerButton button);
|
|
86
|
+
void dispatchTouchCancel();
|
|
87
|
+
|
|
88
|
+
// The hover half. Separate from dispatchTouchMove because the two answer
|
|
89
|
+
// different questions about the same motion event: the touch model asks which
|
|
90
|
+
// view the finger went down on, and hover asks which views the cursor is
|
|
91
|
+
// inside right now.
|
|
92
|
+
void dispatchHover(double x, double y);
|
|
93
|
+
void dispatchHoverLeave();
|
|
94
|
+
|
|
95
|
+
enum class TouchKind { Start, Move, End, Cancel };
|
|
96
|
+
|
|
97
|
+
// Hands the pointer to a gesture recogniser that has activated, cancelling
|
|
98
|
+
// React Native's touch. True when that happened, which means the caller has
|
|
99
|
+
// nothing left to report.
|
|
100
|
+
bool yieldToGesture(double x, double y);
|
|
101
|
+
|
|
102
|
+
// Builds the TouchEvent for the current gesture. React Native wants three
|
|
103
|
+
// lists -- all touches, the ones that changed, and the ones that started on
|
|
104
|
+
// the event target -- which with one pointer are the same list, except on
|
|
105
|
+
// touchend where nothing is touching any more.
|
|
106
|
+
void emit(TouchKind kind, facebook::react::Tag target, double x, double y);
|
|
107
|
+
|
|
108
|
+
// A pointerMove at the view under the cursor. The only pointer event a host
|
|
109
|
+
// emits for hover; see core/HoverTracker.h.
|
|
110
|
+
// A press or a release as a pointer event, which is the only form a
|
|
111
|
+
// non-primary click arrives in.
|
|
112
|
+
void emitPointerButton(bool down,
|
|
113
|
+
facebook::react::Tag target,
|
|
114
|
+
double originX,
|
|
115
|
+
double originY,
|
|
116
|
+
double x,
|
|
117
|
+
double y,
|
|
118
|
+
basalt::PointerButton button);
|
|
119
|
+
void emitPointerMove(
|
|
120
|
+
facebook::react::Tag target, double originX, double originY, double x, double y);
|
|
121
|
+
|
|
122
|
+
GtkMountingManager *mountingManager_;
|
|
123
|
+
RnView *surfaceRoot_;
|
|
124
|
+
|
|
125
|
+
GtkGesture *clickGesture_{nullptr};
|
|
126
|
+
GtkEventController *motionController_{nullptr};
|
|
127
|
+
|
|
128
|
+
// The view a gesture started on. React Native reports every touch in a
|
|
129
|
+
// gesture against the target it began on, even after the pointer leaves that
|
|
130
|
+
// view, because that is what the responder system expects.
|
|
131
|
+
facebook::react::Tag activeTarget_{0};
|
|
132
|
+
bool isDown_{false};
|
|
133
|
+
|
|
134
|
+
// Where the cursor was, so that enter and leave can be told from over and
|
|
135
|
+
// out. See core/HoverTracker.h.
|
|
136
|
+
HoverTracker hover_;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
} // namespace basalt
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// The Linux half of core/WindowControl.h.
|
|
2
|
+
//
|
|
3
|
+
// GTK4 is the most opinionated of the three desktops about this, and several of
|
|
4
|
+
// these functions are shaped by what it will not do. `windowCapabilities()` is
|
|
5
|
+
// how an app finds that out without reading this file.
|
|
6
|
+
//
|
|
7
|
+
// **A window cannot be positioned.** `gtk_window_move` is gone in GTK4 and
|
|
8
|
+
// nothing replaced it: on Wayland a client has no idea where its window is and
|
|
9
|
+
// no way to say, and GTK removed the X11 call rather than ship an API that
|
|
10
|
+
// works on one display server. So `setWindowPosition` and the `x`/`y` in
|
|
11
|
+
// `windowBounds` are honest zeroes, and `centerWindow` is the window manager's
|
|
12
|
+
// job -- which, on both display servers, it does by default for a window that
|
|
13
|
+
// has not been placed.
|
|
14
|
+
//
|
|
15
|
+
// **A window's size is a request.** `gtk_window_set_default_size` is what a
|
|
16
|
+
// client has; the compositor may ignore it. That is also true of the other two
|
|
17
|
+
// desktops in a tiling window manager, and the answer is the same: ask, and
|
|
18
|
+
// report what actually happened through the bounds listener.
|
|
19
|
+
//
|
|
20
|
+
// **A window has no maximum size and does not float.**
|
|
21
|
+
// `gtk_window_set_geometry_hints` and `gtk_window_set_keep_above` were both
|
|
22
|
+
// removed in GTK4, for the same reason as `gtk_window_move`: Wayland has no
|
|
23
|
+
// protocol for either, and an API that worked on X11 alone would be one that
|
|
24
|
+
// silently does nothing half the time -- which is the thing GTK4 decided not to
|
|
25
|
+
// ship. A minimum *is* expressible, because it is a property of the widget
|
|
26
|
+
// rather than a request to the compositor.
|
|
27
|
+
|
|
28
|
+
#include <vector>
|
|
29
|
+
#include "WindowControl.h"
|
|
30
|
+
|
|
31
|
+
#include "GtkTitleBar.h"
|
|
32
|
+
|
|
33
|
+
#include <gtk/gtk.h>
|
|
34
|
+
|
|
35
|
+
namespace basalt {
|
|
36
|
+
|
|
37
|
+
namespace {
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// The window this process owns. The same question PlatformServicesGtk.cpp asks
|
|
41
|
+
// for a popup's parent, and asked the same way: GTK has a list of toplevels
|
|
42
|
+
// rather than an idea of "the app's window".
|
|
43
|
+
GtkWindow *window() {
|
|
44
|
+
GListModel *toplevels = gtk_window_get_toplevels();
|
|
45
|
+
if (toplevels == nullptr) {
|
|
46
|
+
return nullptr;
|
|
47
|
+
}
|
|
48
|
+
GtkWindow *fallback = nullptr;
|
|
49
|
+
const guint count = g_list_model_get_n_items(toplevels);
|
|
50
|
+
for (guint i = 0; i < count; i++) {
|
|
51
|
+
auto *candidate = static_cast<GtkWindow *>(g_list_model_get_item(toplevels, i));
|
|
52
|
+
if (candidate == nullptr) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const bool visible = gtk_widget_get_visible(GTK_WIDGET(candidate)) != FALSE;
|
|
56
|
+
if (visible && gtk_window_is_active(candidate)) {
|
|
57
|
+
g_object_unref(candidate);
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
if (visible && fallback == nullptr) {
|
|
61
|
+
fallback = candidate;
|
|
62
|
+
}
|
|
63
|
+
g_object_unref(candidate);
|
|
64
|
+
}
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
} // namespace
|
|
69
|
+
|
|
70
|
+
// The displays, and the three things GTK 4 will not say about them.
|
|
71
|
+
//
|
|
72
|
+
// `gdk_monitor_get_workarea`, `gdk_display_get_primary_monitor` and any way to
|
|
73
|
+
// ask where the pointer is were all in GTK 3 and are all gone -- not
|
|
74
|
+
// overlooked, removed. Wayland has no protocol for a work area a client can
|
|
75
|
+
// read, no notion of a primary output, and tells a client the pointer's
|
|
76
|
+
// position only while it is over one of that client's surfaces. GTK 4 dropped
|
|
77
|
+
// the X11-only versions rather than offer an API that answers on one display
|
|
78
|
+
// server and not the other.
|
|
79
|
+
//
|
|
80
|
+
// So the work area is reported as the full bounds, the first monitor is
|
|
81
|
+
// reported as primary, and the pointer is reported as unknown. Each is a
|
|
82
|
+
// truthful answer to a question this desktop does not answer, which is the
|
|
83
|
+
// same rule `WindowCapabilities` follows for position and always-on-top.
|
|
84
|
+
std::vector<DisplayInfo> displays() {
|
|
85
|
+
std::vector<DisplayInfo> found;
|
|
86
|
+
GdkDisplay *display = gdk_display_get_default();
|
|
87
|
+
if (display == nullptr) {
|
|
88
|
+
return found;
|
|
89
|
+
}
|
|
90
|
+
GListModel *monitors = gdk_display_get_monitors(display);
|
|
91
|
+
if (monitors == nullptr) {
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
const guint count = g_list_model_get_n_items(monitors);
|
|
95
|
+
for (guint i = 0; i < count; i++) {
|
|
96
|
+
auto *monitor = static_cast<GdkMonitor *>(g_list_model_get_item(monitors, i));
|
|
97
|
+
if (monitor == nullptr) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (gdk_monitor_is_valid(monitor)) {
|
|
101
|
+
GdkRectangle geometry{};
|
|
102
|
+
gdk_monitor_get_geometry(monitor, &geometry);
|
|
103
|
+
|
|
104
|
+
DisplayInfo info;
|
|
105
|
+
info.x = geometry.x;
|
|
106
|
+
info.y = geometry.y;
|
|
107
|
+
info.width = geometry.width;
|
|
108
|
+
info.height = geometry.height;
|
|
109
|
+
// No work area to ask for; see above.
|
|
110
|
+
info.workX = info.x;
|
|
111
|
+
info.workY = info.y;
|
|
112
|
+
info.workWidth = info.width;
|
|
113
|
+
info.workHeight = info.height;
|
|
114
|
+
// `gdk_monitor_get_scale`, not `get_scale_factor`: the factor is an
|
|
115
|
+
// integer and a display at 150% is real, which is the case the integer
|
|
116
|
+
// rounds away.
|
|
117
|
+
const double scale = gdk_monitor_get_scale(monitor);
|
|
118
|
+
info.scaleFactor = scale > 0.0 ? scale : 1.0;
|
|
119
|
+
info.primary = found.empty();
|
|
120
|
+
found.push_back(info);
|
|
121
|
+
}
|
|
122
|
+
g_object_unref(monitor);
|
|
123
|
+
}
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
PointerPosition pointerPosition() {
|
|
128
|
+
// Unknown, and not a gap this host can fill: see above.
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
WindowBounds windowBounds() {
|
|
133
|
+
WindowBounds bounds;
|
|
134
|
+
GtkWindow *target = window();
|
|
135
|
+
if (target == nullptr) {
|
|
136
|
+
return bounds;
|
|
137
|
+
}
|
|
138
|
+
bounds.width = gtk_widget_get_width(GTK_WIDGET(target));
|
|
139
|
+
bounds.height = gtk_widget_get_height(GTK_WIDGET(target));
|
|
140
|
+
bounds.fullScreen = gtk_window_is_fullscreen(target) != FALSE;
|
|
141
|
+
bounds.maximized = gtk_window_is_maximized(target) != FALSE;
|
|
142
|
+
// x and y stay zero. See the header of this file: GTK4 has no way to ask.
|
|
143
|
+
return bounds;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void setWindowSize(double width, double height) {
|
|
147
|
+
GtkWindow *target = window();
|
|
148
|
+
if (target == nullptr || width <= 0.0 || height <= 0.0) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// `default_size` rather than a resize call, because that is the only one GTK4
|
|
152
|
+
// has: it is the size the window takes when it is not being managed into
|
|
153
|
+
// something else, and setting it on a mapped window resizes it.
|
|
154
|
+
gtk_window_set_default_size(target, static_cast<int>(width), static_cast<int>(height));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
void setWindowPosition(double /*x*/, double /*y*/) {
|
|
158
|
+
// Deliberately nothing. See the header: GTK4 removed `gtk_window_move` and
|
|
159
|
+
// Wayland has no equivalent, so an app asking for a position is asking for
|
|
160
|
+
// something no Linux desktop will do. Failing silently is the same answer the
|
|
161
|
+
// compositor gives and is better than a warning an app cannot act on.
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
void centerWindow() {
|
|
165
|
+
// Also nothing, and for the same reason -- centring is positioning. Both
|
|
166
|
+
// display servers centre an unplaced window by default, so an app that never
|
|
167
|
+
// moves its window already gets what this would have asked for.
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
void setWindowFullScreen(bool fullScreen) {
|
|
171
|
+
GtkWindow *target = window();
|
|
172
|
+
if (target == nullptr) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (fullScreen) {
|
|
176
|
+
gtk_window_fullscreen(target);
|
|
177
|
+
} else {
|
|
178
|
+
gtk_window_unfullscreen(target);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
void applyWindowSizeLimits() {
|
|
184
|
+
GtkWindow *target = window();
|
|
185
|
+
if (target == nullptr) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// The minimum only, and through the widget rather than the window manager:
|
|
189
|
+
// GTK works out a toplevel's minimum from what its content needs, and this is
|
|
190
|
+
// how a client adds to that. -1 is GTK's "no request", which is what a
|
|
191
|
+
// cleared limit means.
|
|
192
|
+
const WindowSizeLimits limits = windowSizeLimits();
|
|
193
|
+
gtk_widget_set_size_request(GTK_WIDGET(target),
|
|
194
|
+
limits.minWidth > 0.0 ? static_cast<int>(limits.minWidth) : -1,
|
|
195
|
+
limits.minHeight > 0.0 ? static_cast<int>(limits.minHeight) : -1);
|
|
196
|
+
// The maximum is dropped rather than approximated. A window that enforced one
|
|
197
|
+
// by resizing itself back down would fight the person dragging its corner,
|
|
198
|
+
// which is worse than not having the feature; `windowCapabilities()` says so
|
|
199
|
+
// instead.
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
void setWindowResizable(bool resizable) {
|
|
203
|
+
GtkWindow *target = window();
|
|
204
|
+
if (target != nullptr) {
|
|
205
|
+
gtk_window_set_resizable(target, resizable ? TRUE : FALSE);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
void setWindowAlwaysOnTop(bool /*alwaysOnTop*/) {
|
|
210
|
+
// Nothing to do, and that is the answer rather than a gap. See the header.
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
WindowCapabilities windowCapabilities() {
|
|
214
|
+
WindowCapabilities capabilities;
|
|
215
|
+
capabilities.position = false;
|
|
216
|
+
capabilities.minimumSize = true;
|
|
217
|
+
capabilities.maximumSize = false;
|
|
218
|
+
capabilities.resizable = true;
|
|
219
|
+
capabilities.alwaysOnTop = false;
|
|
220
|
+
return capabilities;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
} // namespace basalt
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#include "GtkWindowModule.h"
|
|
2
|
+
|
|
3
|
+
#include "WindowControl.h"
|
|
4
|
+
#include "WindowMethods.h"
|
|
5
|
+
#include "GtkTitleBar.h"
|
|
6
|
+
#include "PlatformServices.h"
|
|
7
|
+
|
|
8
|
+
#include <cstdint>
|
|
9
|
+
#include <optional>
|
|
10
|
+
#include <string>
|
|
11
|
+
#include <utility>
|
|
12
|
+
#include <vector>
|
|
13
|
+
|
|
14
|
+
namespace basalt {
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
using facebook::jsi::Object;
|
|
19
|
+
using facebook::jsi::Runtime;
|
|
20
|
+
using facebook::jsi::String;
|
|
21
|
+
using facebook::jsi::Value;
|
|
22
|
+
using facebook::react::TurboModule;
|
|
23
|
+
|
|
24
|
+
constexpr const char *kMetricsChangedEvent = "basaltTitleBarMetricsChanged";
|
|
25
|
+
|
|
26
|
+
// processColor's 0xAARRGGBB, or nullopt for anything that is not a number --
|
|
27
|
+
// which is how JavaScript asks for the system's colour back.
|
|
28
|
+
std::optional<uint32_t> colorArgument(const Value *args, size_t count, size_t index) {
|
|
29
|
+
if (index >= count || !args[index].isNumber()) {
|
|
30
|
+
return std::nullopt;
|
|
31
|
+
}
|
|
32
|
+
return static_cast<uint32_t>(static_cast<int64_t>(args[index].asNumber()));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
Value metricsValue(Runtime &runtime, const TitleBarMetrics &metrics) {
|
|
36
|
+
Object object(runtime);
|
|
37
|
+
object.setProperty(runtime, "height", metrics.height);
|
|
38
|
+
object.setProperty(runtime, "buttonsWidth", metrics.buttonsWidth);
|
|
39
|
+
object.setProperty(runtime,
|
|
40
|
+
"style",
|
|
41
|
+
String::createFromAscii(
|
|
42
|
+
runtime, metrics.style == TitleBarStyle::Hidden ? "hidden" : "native"));
|
|
43
|
+
return Value(runtime, object);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
} // namespace
|
|
47
|
+
|
|
48
|
+
GtkWindowModule::GtkWindowModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
|
|
49
|
+
: TurboModule(kModuleName, std::move(jsInvoker)) {
|
|
50
|
+
methodMap_["setTitle"] = MethodMetadata{1, setTitle};
|
|
51
|
+
methodMap_["setTitleBarColors"] = MethodMetadata{3, setTitleBarColors};
|
|
52
|
+
methodMap_["setTitleBarStyle"] = MethodMetadata{1, setTitleBarStyle};
|
|
53
|
+
methodMap_["getTitleBarMetrics"] = MethodMetadata{0, getTitleBarMetrics};
|
|
54
|
+
methodMap_["minimize"] = MethodMetadata{0, minimize};
|
|
55
|
+
methodMap_["toggleMaximize"] = MethodMetadata{0, toggleMaximize};
|
|
56
|
+
methodMap_["close"] = MethodMetadata{0, closeWindow};
|
|
57
|
+
methodMap_["startWindowDrag"] = MethodMetadata{0, startWindowDrag};
|
|
58
|
+
// A window's geometry. The bodies are in core/WindowMethods.h, written once:
|
|
59
|
+
// nothing about reading two numbers or shaping the answer differs per
|
|
60
|
+
// desktop, and three copies would be three chances to drift.
|
|
61
|
+
methodMap_["setSize"] = MethodMetadata{2, windowSetSize};
|
|
62
|
+
methodMap_["setPosition"] = MethodMetadata{2, windowSetPosition};
|
|
63
|
+
methodMap_["center"] = MethodMetadata{0, windowCenter};
|
|
64
|
+
methodMap_["setFullScreen"] = MethodMetadata{1, windowSetFullScreen};
|
|
65
|
+
// How big it may be, whether it may be resized at all, and whether it floats.
|
|
66
|
+
// Not every desktop does every one of these; `getCapabilities` is how an app
|
|
67
|
+
// finds out rather than guessing. See core/WindowControl.h.
|
|
68
|
+
methodMap_["setMinimumSize"] = MethodMetadata{2, windowSetMinimumSize};
|
|
69
|
+
methodMap_["setMaximumSize"] = MethodMetadata{2, windowSetMaximumSize};
|
|
70
|
+
methodMap_["setResizable"] = MethodMetadata{1, windowSetResizable};
|
|
71
|
+
methodMap_["setAlwaysOnTop"] = MethodMetadata{1, windowSetAlwaysOnTop};
|
|
72
|
+
methodMap_["getCapabilities"] = MethodMetadata{0, windowGetCapabilities};
|
|
73
|
+
methodMap_["getBounds"] = MethodMetadata{0, windowGetBounds};
|
|
74
|
+
// What a NativeEventEmitter over this module calls; the events go out as
|
|
75
|
+
// device events either way.
|
|
76
|
+
methodMap_["addListener"] = MethodMetadata{1, noop};
|
|
77
|
+
methodMap_["removeListeners"] = MethodMetadata{1, noop};
|
|
78
|
+
|
|
79
|
+
titleBar().setMetricsListener([this](const TitleBarMetrics &metrics) {
|
|
80
|
+
emitDeviceEvent(kMetricsChangedEvent, [metrics](Runtime &runtime, std::vector<Value> &args) {
|
|
81
|
+
args.emplace_back(metricsValue(runtime, metrics));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The window's geometry, the same way the title bar's metrics go out: a
|
|
86
|
+
// device event, so `useWindow()` can hand an app live bounds rather than a
|
|
87
|
+
// snapshot it has to remember to refresh.
|
|
88
|
+
setWindowBoundsListener([this](const WindowBounds &bounds) {
|
|
89
|
+
emitDeviceEvent(kWindowBoundsEvent, [bounds](Runtime &runtime, std::vector<Value> &args) {
|
|
90
|
+
Object object(runtime);
|
|
91
|
+
object.setProperty(runtime, "x", bounds.x);
|
|
92
|
+
object.setProperty(runtime, "y", bounds.y);
|
|
93
|
+
object.setProperty(runtime, "width", bounds.width);
|
|
94
|
+
object.setProperty(runtime, "height", bounds.height);
|
|
95
|
+
object.setProperty(runtime, "fullScreen", bounds.fullScreen);
|
|
96
|
+
object.setProperty(runtime, "maximized", bounds.maximized);
|
|
97
|
+
args.emplace_back(Value(runtime, object));
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// And ask for the bounds once, now that something is listening. The host
|
|
102
|
+
// primes the cache when its window appears, but this module is built lazily
|
|
103
|
+
// -- the first time JavaScript asks for it -- so an app that mounts before
|
|
104
|
+
// then would read a window of no size and never be told otherwise.
|
|
105
|
+
postToUiThread([] { notifyWindowBoundsChanged(); });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
GtkWindowModule::~GtkWindowModule() {
|
|
109
|
+
titleBar().setMetricsListener(nullptr);
|
|
110
|
+
setWindowBoundsListener(nullptr);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Value GtkWindowModule::setTitle(Runtime &runtime,
|
|
114
|
+
TurboModule & /*module*/,
|
|
115
|
+
const Value *args,
|
|
116
|
+
size_t count) {
|
|
117
|
+
std::optional<std::string> title;
|
|
118
|
+
if (count > 0 && args[0].isString()) {
|
|
119
|
+
title = args[0].getString(runtime).utf8(runtime);
|
|
120
|
+
}
|
|
121
|
+
postToUiThread([title = std::move(title)]() mutable { titleBar().setTitle(std::move(title)); });
|
|
122
|
+
return Value::undefined();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Value GtkWindowModule::setTitleBarColors(Runtime & /*runtime*/,
|
|
126
|
+
TurboModule & /*module*/,
|
|
127
|
+
const Value *args,
|
|
128
|
+
size_t count) {
|
|
129
|
+
const auto background = colorArgument(args, count, 0);
|
|
130
|
+
const auto text = colorArgument(args, count, 1);
|
|
131
|
+
const auto border = colorArgument(args, count, 2);
|
|
132
|
+
postToUiThread([background, text, border] { titleBar().setColors(background, text, border); });
|
|
133
|
+
return Value::undefined();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Value GtkWindowModule::setTitleBarStyle(Runtime &runtime,
|
|
137
|
+
TurboModule & /*module*/,
|
|
138
|
+
const Value *args,
|
|
139
|
+
size_t count) {
|
|
140
|
+
const bool hidden =
|
|
141
|
+
count > 0 && args[0].isString() && args[0].getString(runtime).utf8(runtime) == "hidden";
|
|
142
|
+
postToUiThread([hidden] {
|
|
143
|
+
titleBar().setStyle(hidden ? TitleBarStyle::Hidden : TitleBarStyle::Native);
|
|
144
|
+
});
|
|
145
|
+
return Value::undefined();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Value GtkWindowModule::getTitleBarMetrics(Runtime &runtime,
|
|
149
|
+
TurboModule & /*module*/,
|
|
150
|
+
const Value * /*args*/,
|
|
151
|
+
size_t /*count*/) {
|
|
152
|
+
return metricsValue(runtime, titleBar().metrics());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Value GtkWindowModule::minimize(Runtime & /*runtime*/,
|
|
156
|
+
TurboModule & /*module*/,
|
|
157
|
+
const Value * /*args*/,
|
|
158
|
+
size_t /*count*/) {
|
|
159
|
+
postToUiThread([] { titleBar().minimize(); });
|
|
160
|
+
return Value::undefined();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
Value GtkWindowModule::toggleMaximize(Runtime & /*runtime*/,
|
|
164
|
+
TurboModule & /*module*/,
|
|
165
|
+
const Value * /*args*/,
|
|
166
|
+
size_t /*count*/) {
|
|
167
|
+
postToUiThread([] { titleBar().toggleMaximize(); });
|
|
168
|
+
return Value::undefined();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Value GtkWindowModule::closeWindow(Runtime & /*runtime*/,
|
|
172
|
+
TurboModule & /*module*/,
|
|
173
|
+
const Value * /*args*/,
|
|
174
|
+
size_t /*count*/) {
|
|
175
|
+
postToUiThread([] { titleBar().close(); });
|
|
176
|
+
return Value::undefined();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
Value GtkWindowModule::startWindowDrag(Runtime & /*runtime*/,
|
|
180
|
+
TurboModule & /*module*/,
|
|
181
|
+
const Value * /*args*/,
|
|
182
|
+
size_t /*count*/) {
|
|
183
|
+
postToUiThread([] { titleBar().startDrag(); });
|
|
184
|
+
return Value::undefined();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Value GtkWindowModule::noop(Runtime & /*runtime*/,
|
|
188
|
+
TurboModule & /*module*/,
|
|
189
|
+
const Value * /*args*/,
|
|
190
|
+
size_t /*count*/) {
|
|
191
|
+
return Value::undefined();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
} // namespace basalt
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `BasaltWindow` on Linux: the module src/TitleBar.js talks to.
|
|
2
|
+
//
|
|
3
|
+
// The same four methods the Windows and macOS modules have, because the
|
|
4
|
+
// JavaScript is shared and does not know which desktop it is on. What differs
|
|
5
|
+
// is underneath, in GtkTitleBar.h.
|
|
6
|
+
|
|
7
|
+
#pragma once
|
|
8
|
+
|
|
9
|
+
#include <ReactCommon/TurboModule.h>
|
|
10
|
+
|
|
11
|
+
#include <memory>
|
|
12
|
+
|
|
13
|
+
namespace basalt {
|
|
14
|
+
|
|
15
|
+
class GtkWindowModule : public facebook::react::TurboModule {
|
|
16
|
+
public:
|
|
17
|
+
static constexpr const char *kModuleName = "BasaltWindow";
|
|
18
|
+
|
|
19
|
+
explicit GtkWindowModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
|
|
20
|
+
~GtkWindowModule() override;
|
|
21
|
+
|
|
22
|
+
private:
|
|
23
|
+
static facebook::jsi::Value setTitle(facebook::jsi::Runtime &runtime,
|
|
24
|
+
facebook::react::TurboModule &module,
|
|
25
|
+
const facebook::jsi::Value *args,
|
|
26
|
+
size_t count);
|
|
27
|
+
static facebook::jsi::Value setTitleBarColors(facebook::jsi::Runtime &runtime,
|
|
28
|
+
facebook::react::TurboModule &module,
|
|
29
|
+
const facebook::jsi::Value *args,
|
|
30
|
+
size_t count);
|
|
31
|
+
static facebook::jsi::Value setTitleBarStyle(facebook::jsi::Runtime &runtime,
|
|
32
|
+
facebook::react::TurboModule &module,
|
|
33
|
+
const facebook::jsi::Value *args,
|
|
34
|
+
size_t count);
|
|
35
|
+
static facebook::jsi::Value getTitleBarMetrics(facebook::jsi::Runtime &runtime,
|
|
36
|
+
facebook::react::TurboModule &module,
|
|
37
|
+
const facebook::jsi::Value *args,
|
|
38
|
+
size_t count);
|
|
39
|
+
// The caption buttons' functions, for an app drawing its own header.
|
|
40
|
+
static facebook::jsi::Value minimize(facebook::jsi::Runtime &runtime,
|
|
41
|
+
facebook::react::TurboModule &module,
|
|
42
|
+
const facebook::jsi::Value *args,
|
|
43
|
+
size_t count);
|
|
44
|
+
static facebook::jsi::Value toggleMaximize(facebook::jsi::Runtime &runtime,
|
|
45
|
+
facebook::react::TurboModule &module,
|
|
46
|
+
const facebook::jsi::Value *args,
|
|
47
|
+
size_t count);
|
|
48
|
+
static facebook::jsi::Value closeWindow(facebook::jsi::Runtime &runtime,
|
|
49
|
+
facebook::react::TurboModule &module,
|
|
50
|
+
const facebook::jsi::Value *args,
|
|
51
|
+
size_t count);
|
|
52
|
+
static facebook::jsi::Value startWindowDrag(facebook::jsi::Runtime &runtime,
|
|
53
|
+
facebook::react::TurboModule &module,
|
|
54
|
+
const facebook::jsi::Value *args,
|
|
55
|
+
size_t count);
|
|
56
|
+
static facebook::jsi::Value noop(facebook::jsi::Runtime &runtime,
|
|
57
|
+
facebook::react::TurboModule &module,
|
|
58
|
+
const facebook::jsi::Value *args,
|
|
59
|
+
size_t count);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
} // namespace basalt
|