basalt-appkit 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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runMacos.js +33 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +298 -0
  5. package/native/appkit/AppKitAnimationChoreographer.h +54 -0
  6. package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
  7. package/native/appkit/AppKitDropTarget.h +34 -0
  8. package/native/appkit/AppKitDropTarget.mm +292 -0
  9. package/native/appkit/AppKitFocus.h +120 -0
  10. package/native/appkit/AppKitFocus.mm +293 -0
  11. package/native/appkit/AppKitImageLoader.h +80 -0
  12. package/native/appkit/AppKitImageLoader.mm +156 -0
  13. package/native/appkit/AppKitKeyEvents.h +26 -0
  14. package/native/appkit/AppKitKeyEvents.mm +97 -0
  15. package/native/appkit/AppKitMenuBar.mm +424 -0
  16. package/native/appkit/AppKitMountingManager.h +135 -0
  17. package/native/appkit/AppKitMountingManager.mm +946 -0
  18. package/native/appkit/AppKitRunLoopObserver.h +34 -0
  19. package/native/appkit/AppKitRunLoopObserver.mm +42 -0
  20. package/native/appkit/AppKitScrollView.h +168 -0
  21. package/native/appkit/AppKitScrollView.mm +550 -0
  22. package/native/appkit/AppKitSkiaContext.h +33 -0
  23. package/native/appkit/AppKitSkiaContext.mm +255 -0
  24. package/native/appkit/AppKitSkiaModule.h +53 -0
  25. package/native/appkit/AppKitSkiaModule.mm +81 -0
  26. package/native/appkit/AppKitSkiaPeer.h +37 -0
  27. package/native/appkit/AppKitSkiaPeer.mm +123 -0
  28. package/native/appkit/AppKitSkiaView.h +62 -0
  29. package/native/appkit/AppKitSnapshot.h +21 -0
  30. package/native/appkit/AppKitSnapshot.mm +91 -0
  31. package/native/appkit/AppKitTextInput.h +140 -0
  32. package/native/appkit/AppKitTextInput.mm +588 -0
  33. package/native/appkit/AppKitTextPeer.h +74 -0
  34. package/native/appkit/AppKitTextPeer.mm +353 -0
  35. package/native/appkit/AppKitTitleBar.h +90 -0
  36. package/native/appkit/AppKitTitleBar.mm +186 -0
  37. package/native/appkit/AppKitTouchDispatcher.h +138 -0
  38. package/native/appkit/AppKitTouchDispatcher.mm +514 -0
  39. package/native/appkit/AppKitWindowControl.mm +222 -0
  40. package/native/appkit/AppKitWindowModule.h +62 -0
  41. package/native/appkit/AppKitWindowModule.mm +194 -0
  42. package/native/appkit/ColorSchemeAppKit.mm +71 -0
  43. package/native/appkit/ComponentRegistryAppKit.mm +121 -0
  44. package/native/appkit/CoreTextLayout.h +43 -0
  45. package/native/appkit/CoreTextLayout.mm +252 -0
  46. package/native/appkit/CoreTextLayoutManager.mm +138 -0
  47. package/native/appkit/FontRegistryCoreText.mm +148 -0
  48. package/native/appkit/PlatformServicesAppKit.mm +540 -0
  49. package/native/appkit/RnAppKitView.h +417 -0
  50. package/native/appkit/RnAppKitView.mm +1605 -0
  51. package/native/appkit/RnTextLayout.h +38 -0
  52. package/native/appkit/RnTextLayout.mm +168 -0
  53. package/native/appkit/demo_layout_appkit.mm +93 -0
  54. package/native/appkit/main_appkit.mm +1860 -0
  55. package/native/appkit/mount_harness_appkit.mm +197 -0
  56. package/native/tests/test_appkit_accessibility.mm +170 -0
  57. package/native/tests/test_appkit_image.mm +237 -0
  58. package/native/tests/test_appkit_input.mm +852 -0
  59. package/native/tests/test_appkit_key_events.mm +150 -0
  60. package/native/tests/test_appkit_mounting.mm +430 -0
  61. package/native/tests/test_appkit_scrollbar.mm +132 -0
  62. package/native/tests/test_appkit_services.mm +57 -0
  63. package/native/tests/test_appkit_skia.mm +212 -0
  64. package/native/tests/test_appkit_text.mm +180 -0
  65. package/native/tests/test_appkit_textinput.mm +748 -0
  66. package/native/tests/test_appkit_titlebar.mm +107 -0
  67. package/native/tests/test_appkit_view.mm +237 -0
  68. package/package.json +26 -0
  69. package/react-native.config.js +22 -0
@@ -0,0 +1,353 @@
1
+ #import "AppKitTextPeer.h"
2
+
3
+ #import <objc/runtime.h>
4
+
5
+ // Both peers report gaining focus the same way, and neither superclass does it
6
+ // for us. See the protocol's header comment for why only the gaining half.
7
+ @interface RnAppKitTextField : NSTextField
8
+ @property(nonatomic, assign) NSInteger rnTag;
9
+ @property(nonatomic, weak) id<RnAppKitTextPeerOwner> rnOwner;
10
+ @end
11
+
12
+ @implementation RnAppKitTextField
13
+ - (BOOL)becomeFirstResponder {
14
+ const BOOL became = [super becomeFirstResponder];
15
+ if (became) {
16
+ [_rnOwner rnFieldDidBecomeFirstResponder:_rnTag];
17
+ }
18
+ return became;
19
+ }
20
+ @end
21
+
22
+ @interface RnAppKitSecureTextField : NSSecureTextField
23
+ @property(nonatomic, assign) NSInteger rnTag;
24
+ @property(nonatomic, weak) id<RnAppKitTextPeerOwner> rnOwner;
25
+ @end
26
+
27
+ @implementation RnAppKitSecureTextField
28
+ - (BOOL)becomeFirstResponder {
29
+ const BOOL became = [super becomeFirstResponder];
30
+ if (became) {
31
+ [_rnOwner rnFieldDidBecomeFirstResponder:_rnTag];
32
+ }
33
+ return became;
34
+ }
35
+ @end
36
+
37
+ @interface RnAppKitTextView : NSTextView
38
+ @property(nonatomic, assign) NSInteger rnTag;
39
+ @property(nonatomic, weak) id<RnAppKitTextPeerOwner> rnOwner;
40
+ @property(nonatomic, strong, nullable) NSAttributedString *rnPlaceholder;
41
+ @end
42
+
43
+ @implementation RnAppKitTextView
44
+ - (BOOL)becomeFirstResponder {
45
+ const BOOL became = [super becomeFirstResponder];
46
+ if (became) {
47
+ [_rnOwner rnFieldDidBecomeFirstResponder:_rnTag];
48
+ }
49
+ return became;
50
+ }
51
+
52
+ // An NSTextView has no placeholder, so it is drawn. Over the top of whatever
53
+ // the view drew, which is nothing when the field is empty, and only when it
54
+ // is empty -- the one case a placeholder means anything.
55
+ - (void)drawRect:(NSRect)dirtyRect {
56
+ [super drawRect:dirtyRect];
57
+ if (_rnPlaceholder == nil || self.string.length > 0) {
58
+ return;
59
+ }
60
+ // The same origin the first line of real text would take, so the placeholder
61
+ // does not shift when the user starts typing.
62
+ [_rnPlaceholder drawAtPoint:NSMakePoint(self.textContainerInset.width +
63
+ self.textContainer.lineFragmentPadding,
64
+ self.textContainerInset.height)];
65
+ }
66
+
67
+ // Going from empty to one character, and back, changes whether the placeholder
68
+ // should be there at all -- and AppKit has no reason to know that.
69
+ - (void)didChangeText {
70
+ [super didChangeText];
71
+ self.needsDisplay = YES;
72
+ }
73
+ @end
74
+
75
+ NSView *RnPeerNew(BOOL multiline, BOOL secure, NSInteger tag, id owner, id delegate) {
76
+ if (multiline) {
77
+ RnAppKitTextView *view = [[RnAppKitTextView alloc] initWithFrame:NSZeroRect];
78
+ view.rnTag = tag;
79
+ view.rnOwner = owner;
80
+ view.delegate = (id<NSTextViewDelegate>)delegate;
81
+
82
+ // The RnAppKitView behind it draws the background, border and corner
83
+ // radius from React Native's style props, so the peer paints none of its
84
+ // own -- the same reason the single-line field is unbezeled.
85
+ view.drawsBackground = NO;
86
+ view.focusRingType = NSFocusRingTypeNone;
87
+ // No insets of its own: the view already places the peer inside the
88
+ // content inset Yoga resolved.
89
+ view.textContainerInset = NSZeroSize;
90
+ view.textContainer.lineFragmentPadding = 0;
91
+ // Wrap rather than run off the side. The measured box is already tall
92
+ // enough for the wrapped text, because the shadow node measures a
93
+ // multiline input against the real constraints.
94
+ view.textContainer.widthTracksTextView = YES;
95
+ view.horizontallyResizable = NO;
96
+ view.verticallyResizable = YES;
97
+ // Plain text: React Native styles the whole field through props, and rich
98
+ // text would let a paste bring its own font in.
99
+ view.richText = NO;
100
+ view.allowsUndo = YES;
101
+ return view;
102
+ }
103
+
104
+ NSTextField *field;
105
+ if (secure) {
106
+ RnAppKitSecureTextField *secureField =
107
+ [[RnAppKitSecureTextField alloc] initWithFrame:NSZeroRect];
108
+ secureField.rnTag = tag;
109
+ secureField.rnOwner = owner;
110
+ field = secureField;
111
+ } else {
112
+ RnAppKitTextField *plain = [[RnAppKitTextField alloc] initWithFrame:NSZeroRect];
113
+ plain.rnTag = tag;
114
+ plain.rnOwner = owner;
115
+ field = plain;
116
+ }
117
+
118
+ // One line, and clipped rather than wrapped. An NSTextField wraps by default
119
+ // once its text outruns its width, which is not what a single-line
120
+ // <TextInput> does anywhere else -- it scrolls horizontally. Invisible until
121
+ // a field holds more than it can show, which is why it survived this long.
122
+ field.usesSingleLineMode = YES;
123
+ field.lineBreakMode = NSLineBreakByClipping;
124
+ field.cell.scrollable = YES;
125
+ field.cell.wraps = NO;
126
+
127
+ field.bordered = NO;
128
+ field.bezeled = NO;
129
+ field.drawsBackground = NO;
130
+ field.focusRingType = NSFocusRingTypeNone;
131
+ field.delegate = (id<NSTextFieldDelegate>)delegate;
132
+ // Return has to reach doCommandBySelector: rather than being swallowed as a
133
+ // "do nothing" action, which is what an NSTextField with no target does.
134
+ field.target = nil;
135
+ field.action = nullptr;
136
+ return field;
137
+ }
138
+
139
+ BOOL RnPeerIsMultiline(NSView *peer) {
140
+ return peer != nil && [peer isKindOfClass:[NSTextView class]];
141
+ }
142
+
143
+ NSString *RnPeerText(NSView *peer) {
144
+ if (peer == nil) {
145
+ return @"";
146
+ }
147
+ if (RnPeerIsMultiline(peer)) {
148
+ NSString *text = ((NSTextView *)peer).string;
149
+ return text != nil ? text : @"";
150
+ }
151
+ NSString *value = ((NSTextField *)peer).stringValue;
152
+ return value != nil ? value : @"";
153
+ }
154
+
155
+ void RnPeerSetText(NSView *peer, NSString *text) {
156
+ if (peer == nil) {
157
+ return;
158
+ }
159
+ NSString *value = text != nil ? text : @"";
160
+ if (RnPeerIsMultiline(peer)) {
161
+ ((NSTextView *)peer).string = value;
162
+ return;
163
+ }
164
+ ((NSTextField *)peer).stringValue = value;
165
+ }
166
+
167
+ NSRange RnPeerSelection(NSView *peer) {
168
+ if (peer == nil) {
169
+ return NSMakeRange(0, 0);
170
+ }
171
+ if (RnPeerIsMultiline(peer)) {
172
+ // An NSTextView is its own editor, so the selection is always available --
173
+ // which is the one place multiline is simpler than single line here.
174
+ return ((NSTextView *)peer).selectedRange;
175
+ }
176
+ NSTextField *field = (NSTextField *)peer;
177
+ if (NSText *editor = field.currentEditor) {
178
+ return editor.selectedRange;
179
+ }
180
+ return NSMakeRange(field.stringValue.length, 0);
181
+ }
182
+
183
+ void RnPeerSetSelection(NSView *peer, NSRange range) {
184
+ if (peer == nil) {
185
+ return;
186
+ }
187
+ if (RnPeerIsMultiline(peer)) {
188
+ NSTextView *view = (NSTextView *)peer;
189
+ const NSUInteger length = view.string.length;
190
+ const NSUInteger location = MIN(range.location, length);
191
+ [view setSelectedRange:NSMakeRange(location, MIN(range.length, length - location))];
192
+ return;
193
+ }
194
+ if (NSText *editor = ((NSTextField *)peer).currentEditor) {
195
+ editor.selectedRange = range;
196
+ }
197
+ }
198
+
199
+ void RnPeerSetEditable(NSView *peer, BOOL editable) {
200
+ if (peer == nil) {
201
+ return;
202
+ }
203
+ if (RnPeerIsMultiline(peer)) {
204
+ NSTextView *view = (NSTextView *)peer;
205
+ view.editable = editable;
206
+ view.selectable = YES;
207
+ return;
208
+ }
209
+ NSTextField *field = (NSTextField *)peer;
210
+ field.editable = editable;
211
+ field.selectable = YES;
212
+ }
213
+
214
+ void RnPeerSetTextStyle(NSView *peer, NSFont *font, NSColor *colour, NSTextAlignment alignment) {
215
+ if (peer == nil) {
216
+ return;
217
+ }
218
+ if (RnPeerIsMultiline(peer)) {
219
+ NSTextView *view = (NSTextView *)peer;
220
+ if (font != nil) {
221
+ view.font = font;
222
+ }
223
+ if (colour != nil) {
224
+ view.textColor = colour;
225
+ }
226
+ view.alignment = alignment;
227
+ // What typing continues in. Without it the font and colour apply to what
228
+ // is there and the next character reverts to the system default.
229
+ NSMutableDictionary *typing = [NSMutableDictionary dictionary];
230
+ if (font != nil) {
231
+ typing[NSFontAttributeName] = font;
232
+ }
233
+ if (colour != nil) {
234
+ typing[NSForegroundColorAttributeName] = colour;
235
+ }
236
+ view.typingAttributes = typing;
237
+ return;
238
+ }
239
+ NSTextField *field = (NSTextField *)peer;
240
+ if (font != nil) {
241
+ field.font = font;
242
+ }
243
+ if (colour != nil) {
244
+ field.textColor = colour;
245
+ }
246
+ field.alignment = alignment;
247
+ }
248
+
249
+ void RnPeerSetPlaceholder(NSView *peer, NSAttributedString *placeholder) {
250
+ if (peer == nil) {
251
+ return;
252
+ }
253
+ if (RnPeerIsMultiline(peer)) {
254
+ RnAppKitTextView *view = (RnAppKitTextView *)peer;
255
+ view.rnPlaceholder = placeholder;
256
+ view.needsDisplay = YES;
257
+ return;
258
+ }
259
+ ((NSTextField *)peer).placeholderAttributedString = placeholder;
260
+ }
261
+
262
+ NSAttributedString *RnPeerPlaceholder(NSView *peer) {
263
+ if (peer == nil) {
264
+ return nil;
265
+ }
266
+ if (RnPeerIsMultiline(peer)) {
267
+ return ((RnAppKitTextView *)peer).rnPlaceholder;
268
+ }
269
+ return ((NSTextField *)peer).placeholderAttributedString;
270
+ }
271
+
272
+ // A formatter that refuses anything longer than the limit. NSTextField has no
273
+ // maximum length of its own, and a formatter is what AppKit offers instead --
274
+ // it is consulted on every edit, which is exactly the hook needed.
275
+ @interface RnAppKitLengthFormatter : NSFormatter
276
+ @property(nonatomic, assign) NSInteger maxLength;
277
+ @end
278
+
279
+ @implementation RnAppKitLengthFormatter
280
+ - (NSString *)stringForObjectValue:(id)object {
281
+ return [object isKindOfClass:[NSString class]] ? object : nil;
282
+ }
283
+
284
+ - (BOOL)getObjectValue:(out id *)object
285
+ forString:(NSString *)string
286
+ errorDescription:(out NSString **)error {
287
+ if (object != nullptr) {
288
+ *object = string;
289
+ }
290
+ return YES;
291
+ }
292
+
293
+ - (BOOL)isPartialStringValid:(NSString *)partial
294
+ newEditingString:(NSString **)newString
295
+ errorDescription:(NSString **)error {
296
+ if (_maxLength <= 0 || (NSInteger)partial.length <= _maxLength) {
297
+ return YES;
298
+ }
299
+ // NO with no replacement: the edit is refused and what was there stays.
300
+ if (newString != nullptr) {
301
+ *newString = nil;
302
+ }
303
+ if (error != nullptr) {
304
+ *error = nil;
305
+ }
306
+ return NO;
307
+ }
308
+ @end
309
+
310
+ void RnPeerSetMaxLength(NSView *peer, NSInteger maxLength) {
311
+ if (peer == nil) {
312
+ return;
313
+ }
314
+ if (RnPeerIsMultiline(peer)) {
315
+ // Stored, and read back by the delegate through RnPeerAllowsChange. An
316
+ // NSTextView has no formatter.
317
+ objc_setAssociatedObject(peer, @selector(RnPeerSetMaxLength), @(maxLength),
318
+ OBJC_ASSOCIATION_RETAIN_NONATOMIC);
319
+ return;
320
+ }
321
+
322
+ NSTextField *field = (NSTextField *)peer;
323
+ if (maxLength <= 0) {
324
+ field.formatter = nil;
325
+ return;
326
+ }
327
+ RnAppKitLengthFormatter *formatter = [[RnAppKitLengthFormatter alloc] init];
328
+ formatter.maxLength = maxLength;
329
+ field.formatter = formatter;
330
+ }
331
+
332
+ BOOL RnPeerAllowsChange(NSView *peer, NSRange range, NSString *replacement) {
333
+ if (peer == nil || !RnPeerIsMultiline(peer)) {
334
+ return YES;
335
+ }
336
+ NSNumber *limit = objc_getAssociatedObject(peer, @selector(RnPeerSetMaxLength));
337
+ const NSInteger maxLength = limit != nil ? limit.integerValue : 0;
338
+ if (maxLength <= 0) {
339
+ return YES;
340
+ }
341
+ const NSInteger current = (NSInteger)((NSTextView *)peer).string.length;
342
+ const NSInteger after = current - (NSInteger)range.length + (NSInteger)replacement.length;
343
+ return after <= maxLength;
344
+ }
345
+
346
+ NSResponder *RnPeerEditor(NSView *peer) {
347
+ if (peer == nil) {
348
+ return nil;
349
+ }
350
+ // An NSTextView is the editor; an NSTextField borrows one from the window
351
+ // and only while it has focus.
352
+ return RnPeerIsMultiline(peer) ? (NSResponder *)peer : ((NSTextField *)peer).currentEditor;
353
+ }
@@ -0,0 +1,90 @@
1
+ // The window's title bar on macOS: its title, its colours, and whether the
2
+ // system draws it or the app does.
3
+ //
4
+ // The same two styles Win32TitleBar.h describes, and AppKit happens to offer
5
+ // both directly:
6
+ //
7
+ // - Native style. The window keeps its title bar, which sits *outside* the
8
+ // content view -- so it takes nothing from the app and every metric is zero,
9
+ // exactly as on Windows. Colours are the window's background plus a
10
+ // transparent title bar, which is how AppKit lets a caption take an app's
11
+ // colour at all; there is no per-element caption colour the way DWM has one.
12
+ //
13
+ // - Hidden style. NSWindowStyleMaskFullSizeContentView puts the content view
14
+ // under the title bar, so the app's content reaches the top of the window.
15
+ // The traffic lights stay -- the system keeps drawing them, which is the
16
+ // macOS counterpart of the host painting caption buttons on Windows -- and
17
+ // the metrics say how much room to leave for them.
18
+ //
19
+ // Main thread only, like the window. The BasaltWindow module posts here.
20
+
21
+ #pragma once
22
+
23
+ #import <Cocoa/Cocoa.h>
24
+
25
+ #include <cstdint>
26
+ #include <functional>
27
+ #include <optional>
28
+ #include <string>
29
+
30
+ namespace basalt {
31
+
32
+ enum class TitleBarStyle { Native, Hidden };
33
+
34
+ // What JavaScript is told, in points -- which are the surface root's units on
35
+ // this host. All zero while the style is native, because the system's title bar
36
+ // is outside the content view and takes nothing from the app.
37
+ struct TitleBarMetrics {
38
+ double height{0};
39
+ double buttonsWidth{0};
40
+ TitleBarStyle style{TitleBarStyle::Native};
41
+ };
42
+
43
+ class AppKitTitleBar {
44
+ public:
45
+ // The window to talk to. Held weakly: the title bar outlives nothing.
46
+ void attach(NSWindow *window);
47
+
48
+ void setTitle(std::optional<std::string> title);
49
+ // 0xAARRGGBB as processColor produces, or nullopt for the system's own.
50
+ void setColors(std::optional<uint32_t> background,
51
+ std::optional<uint32_t> text,
52
+ std::optional<uint32_t> border);
53
+ void setStyle(TitleBarStyle style);
54
+
55
+ // What the caption buttons do, for an app that draws its own.
56
+ //
57
+ // The system's buttons work without any of this -- they are the system's --
58
+ // but a header the app drew has no way to act on itself otherwise, which
59
+ // makes the hidden style a picture of a title bar rather than one.
60
+ void minimize();
61
+ void toggleMaximize();
62
+ void close();
63
+ // Begins a window drag from the pointer's current position, which is what a
64
+ // caption does when you press it and move. Wanted for the same reason:
65
+ // without it an app-drawn header cannot move its own window.
66
+ void startDrag();
67
+
68
+ TitleBarMetrics metrics() const;
69
+
70
+ // Called whenever the metrics change, so JavaScript can re-lay-out. Cleared
71
+ // by the module's destructor.
72
+ void setMetricsListener(std::function<void(const TitleBarMetrics &)> listener);
73
+
74
+ private:
75
+ void apply();
76
+ void notify();
77
+
78
+ __weak NSWindow *window_{nil};
79
+ std::optional<std::string> title_;
80
+ std::optional<uint32_t> background_;
81
+ std::optional<uint32_t> text_;
82
+ std::optional<uint32_t> border_;
83
+ TitleBarStyle style_{TitleBarStyle::Native};
84
+ std::function<void(const TitleBarMetrics &)> listener_;
85
+ };
86
+
87
+ // The one per process, as the window is.
88
+ AppKitTitleBar &titleBar();
89
+
90
+ } // namespace basalt
@@ -0,0 +1,186 @@
1
+ #import "AppKitTitleBar.h"
2
+
3
+ #include <cstdint>
4
+ #include <functional>
5
+
6
+ namespace basalt {
7
+
8
+ namespace {
9
+
10
+ NSColor *colorFrom(uint32_t argb) {
11
+ // processColor's 0xAARRGGBB. sRGB explicitly, for the reason every other
12
+ // colour on this host is: letting AppKit pick a device space shifts it on a
13
+ // wide-gamut display, and the same app on Linux would not shift.
14
+ return [NSColor colorWithSRGBRed:((argb >> 16) & 0xFF) / 255.0
15
+ green:((argb >> 8) & 0xFF) / 255.0
16
+ blue:(argb & 0xFF) / 255.0
17
+ alpha:((argb >> 24) & 0xFF) / 255.0];
18
+ }
19
+
20
+ // Whether a colour is dark enough to want light text over it. The same
21
+ // question DWMWA_USE_IMMERSIVE_DARK_MODE answers on Windows, asked here
22
+ // because AppKit has no caption text colour: the appearance decides it.
23
+ bool isDark(uint32_t argb) {
24
+ const double r = ((argb >> 16) & 0xFF) / 255.0;
25
+ const double g = ((argb >> 8) & 0xFF) / 255.0;
26
+ const double b = (argb & 0xFF) / 255.0;
27
+ // Rec. 601 luma, which is what every "is this dark" check uses and is close
28
+ // enough for a binary decision.
29
+ return (0.299 * r + 0.587 * g + 0.114 * b) < 0.5;
30
+ }
31
+
32
+ } // namespace
33
+
34
+ void AppKitTitleBar::attach(NSWindow *window) {
35
+ window_ = window;
36
+ apply();
37
+ }
38
+
39
+ void AppKitTitleBar::setTitle(std::optional<std::string> title) {
40
+ title_ = std::move(title);
41
+ apply();
42
+ }
43
+
44
+ void AppKitTitleBar::setColors(std::optional<uint32_t> background,
45
+ std::optional<uint32_t> text,
46
+ std::optional<uint32_t> border) {
47
+ background_ = background;
48
+ text_ = text;
49
+ border_ = border;
50
+ apply();
51
+ }
52
+
53
+ void AppKitTitleBar::setStyle(TitleBarStyle style) {
54
+ if (style_ == style) {
55
+ return;
56
+ }
57
+ style_ = style;
58
+ apply();
59
+ notify();
60
+ }
61
+
62
+ void AppKitTitleBar::minimize() {
63
+ [window_ miniaturize:nil];
64
+ }
65
+
66
+ void AppKitTitleBar::toggleMaximize() {
67
+ // `zoom:` rather than a maximised flag: on macOS the green button toggles
68
+ // between the window's frame and the "best" one for the screen, and there is
69
+ // no maximised state to set.
70
+ [window_ zoom:nil];
71
+ }
72
+
73
+ void AppKitTitleBar::close() {
74
+ // performClose: rather than close:, so the delegate gets its say and the
75
+ // window closes the way the red button closes it.
76
+ [window_ performClose:nil];
77
+ }
78
+
79
+ void AppKitTitleBar::startDrag() {
80
+ NSWindow *window = window_;
81
+ if (window == nil) {
82
+ return;
83
+ }
84
+ // The event currently being handled, which is the press that started this.
85
+ // AppKit takes over the drag from there and runs it until the mouse is
86
+ // released.
87
+ NSEvent *event = NSApp.currentEvent;
88
+ if (event != nil) {
89
+ [window performWindowDragWithEvent:event];
90
+ }
91
+ }
92
+
93
+ TitleBarMetrics AppKitTitleBar::metrics() const {
94
+ TitleBarMetrics result;
95
+ result.style = style_;
96
+ if (style_ == TitleBarStyle::Native) {
97
+ // The system's title bar is outside the content view, so it costs the app
98
+ // nothing and there is nothing to report.
99
+ return result;
100
+ }
101
+
102
+ NSWindow *window = window_;
103
+ if (window == nil) {
104
+ return result;
105
+ }
106
+
107
+ // What the content view gained by extending under the title bar, which is
108
+ // exactly what the app has to leave clear.
109
+ //
110
+ // Not `contentRectForFrameRect:`: with NSWindowStyleMaskFullSizeContentView
111
+ // the content rect *is* the frame, so that difference is zero and says the
112
+ // title bar costs nothing -- which is true of the window and false of the
113
+ // app. `contentLayoutRect` is the part not covered by the title bar, which
114
+ // is the question actually being asked.
115
+ result.height = NSHeight(window.contentView.frame) - NSHeight(window.contentLayoutRect);
116
+
117
+ // The traffic lights, from where they actually are rather than from a
118
+ // constant: their size and spacing are the system's to change.
119
+ NSButton *close = [window standardWindowButton:NSWindowCloseButton];
120
+ NSButton *zoom = [window standardWindowButton:NSWindowZoomButton];
121
+ if (close != nil && zoom != nil) {
122
+ const NSRect closeFrame = close.frame;
123
+ const NSRect zoomFrame = zoom.frame;
124
+ // From the window's left edge to the far side of the last button, which is
125
+ // the width an app has to keep clear -- the buttons sit at the left on
126
+ // macOS, where Windows puts them at the right.
127
+ result.buttonsWidth = NSMaxX(zoomFrame) + NSMinX(closeFrame);
128
+ }
129
+ return result;
130
+ }
131
+
132
+ void AppKitTitleBar::setMetricsListener(std::function<void(const TitleBarMetrics &)> listener) {
133
+ listener_ = std::move(listener);
134
+ }
135
+
136
+ void AppKitTitleBar::apply() {
137
+ NSWindow *window = window_;
138
+ if (window == nil) {
139
+ return;
140
+ }
141
+
142
+ if (title_.has_value()) {
143
+ NSString *value = [NSString stringWithUTF8String:title_->c_str()];
144
+ window.title = value != nil ? value : @"";
145
+ }
146
+
147
+ const bool hidden = style_ == TitleBarStyle::Hidden;
148
+ window.titlebarAppearsTransparent = hidden || background_.has_value();
149
+ // The title text would otherwise sit over the app's own content.
150
+ window.titleVisibility = hidden ? NSWindowTitleHidden : NSWindowTitleVisible;
151
+
152
+ if (hidden) {
153
+ window.styleMask |= NSWindowStyleMaskFullSizeContentView;
154
+ } else {
155
+ window.styleMask &= ~NSWindowStyleMaskFullSizeContentView;
156
+ }
157
+
158
+ if (background_.has_value()) {
159
+ window.backgroundColor = colorFrom(*background_);
160
+ // AppKit has no caption *text* colour: the window's appearance decides it.
161
+ // So a dark caption gets the dark appearance, which is the same decision
162
+ // Windows makes through DWMWA_USE_IMMERSIVE_DARK_MODE -- and honouring an
163
+ // explicit textColor beyond that is not something AppKit offers.
164
+ const bool wantsLight = text_.has_value() ? !isDark(*text_) : isDark(*background_);
165
+ window.appearance = [NSAppearance appearanceNamed:wantsLight ? NSAppearanceNameDarkAqua
166
+ : NSAppearanceNameAqua];
167
+ }
168
+
169
+ // `border` has no AppKit equivalent. A window's edge is the system's, and
170
+ // there is no DWMWA_BORDER_COLOR here -- recorded rather than silently
171
+ // dropped, so the asymmetry is visible where it is decided.
172
+ (void)border_;
173
+ }
174
+
175
+ void AppKitTitleBar::notify() {
176
+ if (listener_) {
177
+ listener_(metrics());
178
+ }
179
+ }
180
+
181
+ AppKitTitleBar &titleBar() {
182
+ static AppKitTitleBar instance;
183
+ return instance;
184
+ }
185
+
186
+ } // namespace basalt