macos inputs
This commit is contained in:
parent
85acfb5b67
commit
c0d923cbed
37
README.md
37
README.md
|
|
@ -18,6 +18,43 @@ ABI headers/protocol bindings and runtime-loaded system libraries. Normal
|
||||||
builds need Zig alone; the optional `shell.nix` supplies maintainer tools and
|
builds need Zig alone; the optional `shell.nix` supplies maintainer tools and
|
||||||
NixOS runtime library paths. See [Linux support and limitations](src/linux/README.md).
|
NixOS runtime library paths. See [Linux support and limitations](src/linux/README.md).
|
||||||
|
|
||||||
|
## Experimental desktop input and window observations
|
||||||
|
|
||||||
|
macOS uses AppKit editing selectors. Windows and Wayland share Ctrl-based defaults
|
||||||
|
for word deletion, paragraph/page movement, clipboard actions, undo/redo, Backtab,
|
||||||
|
Shift+Enter and cancellation. Shift extends selection only for movement. Alt/Super
|
||||||
|
combinations are not interpreted as Ctrl editing shortcuts. Clipboard shortcuts
|
||||||
|
work without a menu, with menu actions taking precedence when available.
|
||||||
|
Applications own execution, Unicode boundaries, layout and undo history. Commands
|
||||||
|
without customary shortcuts (such as line-boundary deletion on Windows) have no
|
||||||
|
invented default binding.
|
||||||
|
|
||||||
|
Publish an acknowledged text snapshot after consuming text/edit events. On macOS,
|
||||||
|
further native text input waits after semantic edits, including ignored commands;
|
||||||
|
acknowledge asynchronous paste after applying its result. Disabling text input or
|
||||||
|
losing window focus drops that deferred input. Windows IMM emits ordered text
|
||||||
|
without surrounding-text replacement ranges. Wayland invalidates old IME snapshot
|
||||||
|
serials after document/selection-changing commands; new surrounding text is
|
||||||
|
published after acknowledgment. It does not apply AppKit's input-queue mechanism.
|
||||||
|
|
||||||
|
`WINDOW_METRICS` is the resize and state-change hook. Geometry state is independent
|
||||||
|
of minimization. Observation bits indicate which facts are known:
|
||||||
|
|
||||||
|
| Backend | Known observations |
|
||||||
|
| --- | --- |
|
||||||
|
| macOS | Minimized, live resize, occluded |
|
||||||
|
| Windows | Minimized, interactive size/move loop |
|
||||||
|
| Wayland | Resizing; rendering suspension with xdg-shell v6+ |
|
||||||
|
|
||||||
|
A clear observation bit means unknown, not false. Wayland cannot report minimized
|
||||||
|
state or directly undo minimization, and suspension is not proof of occlusion.
|
||||||
|
Explicitly unavailable Wayland window-manager actions complete with `UNSUPPORTED`.
|
||||||
|
Render hints do not replace presentation pacing or event processing.
|
||||||
|
|
||||||
|
This library is unreleased and experimental; the changed C struct layout requires
|
||||||
|
rebuilding consumers, without ABI compatibility guarantees. Windows and Linux
|
||||||
|
runtime qualification must accompany cross-compilation checks.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
|
|
@ -324,8 +324,25 @@ typedef struct uhdk_window_desc {
|
||||||
uhdk_window *parent; /* Optional; must outlive child. */
|
uhdk_window *parent; /* Optional; must outlive child. */
|
||||||
} uhdk_window_desc;
|
} uhdk_window_desc;
|
||||||
|
|
||||||
/* On Wayland, visible is application show/hide intent, not compositor visibility.
|
/* Observation bits say which independent window facts are known. A clear bit
|
||||||
* State is the last observable state; Wayland does not report minimization.
|
* means unknown, not false. Support depends on the backend/protocol version. */
|
||||||
|
typedef uint32_t uhdk_window_observation_flags;
|
||||||
|
enum {
|
||||||
|
UHDK_WINDOW_OBSERVE_MINIMIZED = 1u << 0,
|
||||||
|
UHDK_WINDOW_OBSERVE_RESIZING = 1u << 1,
|
||||||
|
UHDK_WINDOW_OBSERVE_OCCLUDED = 1u << 2,
|
||||||
|
UHDK_WINDOW_OBSERVE_SUSPENDED = 1u << 3
|
||||||
|
};
|
||||||
|
|
||||||
|
/* WINDOW_METRICS is the resize/state hook; compare snapshots for transitions.
|
||||||
|
* State describes geometry (normal/maximized/fullscreen), independently of
|
||||||
|
* minimized. Wayland does not report minimization; its observation bit is clear.
|
||||||
|
* visible is show/hide visibility, not an occlusion or minimization guarantee;
|
||||||
|
* on Wayland it is application intent. occluded is an optional rendering hint,
|
||||||
|
* not permission to stop processing events. suspended is a separate compositor
|
||||||
|
* hint that content is not ordinarily repainted, not proof of occlusion or
|
||||||
|
* minimization. Resizing is an advisory interactive size/move-loop
|
||||||
|
* hint, not a guaranteed transaction around every size change.
|
||||||
* Async size/state completion acknowledges submission; geometry arrives here. */
|
* Async size/state completion acknowledges submission; geometry arrives here. */
|
||||||
typedef struct uhdk_window_metrics {
|
typedef struct uhdk_window_metrics {
|
||||||
uint32_t struct_size;
|
uint32_t struct_size;
|
||||||
|
|
@ -336,6 +353,11 @@ typedef struct uhdk_window_metrics {
|
||||||
float scale;
|
float scale;
|
||||||
uhdk_bool focused;
|
uhdk_bool focused;
|
||||||
uhdk_bool visible;
|
uhdk_bool visible;
|
||||||
|
uhdk_window_observation_flags observations;
|
||||||
|
uhdk_bool minimized;
|
||||||
|
uhdk_bool resizing;
|
||||||
|
uhdk_bool occluded;
|
||||||
|
uhdk_bool suspended;
|
||||||
} uhdk_window_metrics;
|
} uhdk_window_metrics;
|
||||||
|
|
||||||
UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async(
|
UHDK_API uhdk_status UHDK_CALL uhdk_window_create_async(
|
||||||
|
|
@ -545,6 +567,11 @@ UHDK_API uhdk_status UHDK_CALL uhdk_window_set_pointer_mode_async(
|
||||||
* revision; do not discard a burst just because its first edit changed revision.
|
* revision; do not discard a burst just because its first edit changed revision.
|
||||||
* acknowledged_input_id is the last applied text/edit input ID. Snapshots behind
|
* acknowledged_input_id is the last applied text/edit input ID. Snapshots behind
|
||||||
* outstanding native edits are ignored; publish again after consuming the batch.
|
* outstanding native edits are ignored; publish again after consuming the batch.
|
||||||
|
* On macOS, a semantic edit invalidates the native snapshot. Subsequent text
|
||||||
|
* input waits for an acknowledging snapshot, including when an edit is ignored.
|
||||||
|
* Acknowledge asynchronous edits (such as paste) only once their result is applied.
|
||||||
|
* Disabling text input discards deferred input. Raw key IDs are observational;
|
||||||
|
* acknowledge applied text/edit events, not raw keys that may precede them.
|
||||||
* Commit/preedit events carry text + an optional replacement range. A preedit
|
* Commit/preedit events carry text + an optional replacement range. A preedit
|
||||||
* updates temporary composition, never the permanent document by itself.
|
* updates temporary composition, never the permanent document by itself.
|
||||||
*/
|
*/
|
||||||
|
|
@ -870,6 +897,19 @@ enum {
|
||||||
UHDK_REQUEST_FILE_INFO
|
UHDK_REQUEST_FILE_INFO
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Semantic requests, not edits performed by UHDK. The application owns text,
|
||||||
|
* grapheme/word boundaries, bidi/layout, selection, clipboard actions and history.
|
||||||
|
* LEFT/RIGHT are visual; BACKWARD/FORWARD follow logical document order.
|
||||||
|
* LINE uses visual lines; LEFT_END/RIGHT_END explicitly select their visual edge.
|
||||||
|
* LINE_START/END follow writing direction; PARAGRAPH uses document boundaries.
|
||||||
|
* PAGE moves the caret by a viewport; SCROLL moves the viewport without the caret.
|
||||||
|
* Deletion first removes the selection; otherwise it removes the indicated unit.
|
||||||
|
* NEWLINE requests a paragraph separator; LINE_BREAK requests a soft line break.
|
||||||
|
* TAB/BACKTAB may insert text or traverse focus, as decided by the application.
|
||||||
|
* CANCEL is delivered only after native composition handling declines/finishes it.
|
||||||
|
* Native default gestures vary: macOS uses AppKit selectors, Windows/Wayland
|
||||||
|
* use Ctrl-based editing defaults. Commands without customary platform gestures
|
||||||
|
* remain application-bindable intents, not invented global shortcuts. */
|
||||||
typedef uint32_t uhdk_edit_command;
|
typedef uint32_t uhdk_edit_command;
|
||||||
enum {
|
enum {
|
||||||
UHDK_EDIT_COPY = 1,
|
UHDK_EDIT_COPY = 1,
|
||||||
|
|
@ -891,7 +931,30 @@ enum {
|
||||||
UHDK_EDIT_WORD_LEFT,
|
UHDK_EDIT_WORD_LEFT,
|
||||||
UHDK_EDIT_WORD_RIGHT,
|
UHDK_EDIT_WORD_RIGHT,
|
||||||
UHDK_EDIT_NEWLINE,
|
UHDK_EDIT_NEWLINE,
|
||||||
UHDK_EDIT_TAB
|
UHDK_EDIT_TAB,
|
||||||
|
UHDK_EDIT_BACKTAB,
|
||||||
|
UHDK_EDIT_LINE_BREAK,
|
||||||
|
UHDK_EDIT_CANCEL,
|
||||||
|
UHDK_EDIT_DELETE_WORD_BACKWARD,
|
||||||
|
UHDK_EDIT_DELETE_WORD_FORWARD,
|
||||||
|
UHDK_EDIT_DELETE_LINE_START,
|
||||||
|
UHDK_EDIT_DELETE_LINE_END,
|
||||||
|
UHDK_EDIT_DELETE_PARAGRAPH_START,
|
||||||
|
UHDK_EDIT_DELETE_PARAGRAPH_END,
|
||||||
|
UHDK_EDIT_MOVE_BACKWARD,
|
||||||
|
UHDK_EDIT_MOVE_FORWARD,
|
||||||
|
UHDK_EDIT_WORD_BACKWARD,
|
||||||
|
UHDK_EDIT_WORD_FORWARD,
|
||||||
|
UHDK_EDIT_PARAGRAPH_START,
|
||||||
|
UHDK_EDIT_PARAGRAPH_END,
|
||||||
|
UHDK_EDIT_PAGE_UP,
|
||||||
|
UHDK_EDIT_PAGE_DOWN,
|
||||||
|
UHDK_EDIT_SCROLL_PAGE_UP,
|
||||||
|
UHDK_EDIT_SCROLL_PAGE_DOWN,
|
||||||
|
UHDK_EDIT_SCROLL_DOCUMENT_START,
|
||||||
|
UHDK_EDIT_SCROLL_DOCUMENT_END,
|
||||||
|
UHDK_EDIT_LINE_LEFT_END,
|
||||||
|
UHDK_EDIT_LINE_RIGHT_END
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Event data is a pointer to the named payload type selected by type.
|
/* Event data is a pointer to the named payload type selected by type.
|
||||||
|
|
@ -939,7 +1002,7 @@ typedef struct uhdk_text_event {
|
||||||
|
|
||||||
typedef struct uhdk_edit_event {
|
typedef struct uhdk_edit_event {
|
||||||
uhdk_edit_command command;
|
uhdk_edit_command command;
|
||||||
uhdk_bool extend_selection;
|
uhdk_bool extend_selection; /* Only meaningful for caret movement. */
|
||||||
} uhdk_edit_event;
|
} uhdk_edit_event;
|
||||||
|
|
||||||
typedef struct uhdk_drop_files_event {
|
typedef struct uhdk_drop_files_event {
|
||||||
|
|
|
||||||
|
|
@ -182,3 +182,40 @@ test "READY precedes native startup events without being published on failed sta
|
||||||
try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_WAKE), preferences.next.?.event.type);
|
try std.testing.expectEqual(@as(u32, c.UHDK_EVENT_WAKE), preferences.next.?.event.type);
|
||||||
try std.testing.expect(application.pending_tail == preferences.next);
|
try std.testing.expect(application.pending_tail == preferences.next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = @import("linux/WindowState.zig");
|
||||||
|
}
|
||||||
|
|
||||||
|
test "desktop edit defaults preserve modifiers and selection intent" {
|
||||||
|
const ctrl = c.UHDK_MOD_CONTROL;
|
||||||
|
const shift = c.UHDK_MOD_SHIFT;
|
||||||
|
const cases = [_][3]u32{
|
||||||
|
.{ c.UHDK_KEY_BACKSPACE, ctrl, c.UHDK_EDIT_DELETE_WORD_BACKWARD },
|
||||||
|
.{ c.UHDK_KEY_DELETE, ctrl, c.UHDK_EDIT_DELETE_WORD_FORWARD },
|
||||||
|
.{ c.UHDK_KEY_DELETE, shift, c.UHDK_EDIT_CUT },
|
||||||
|
.{ c.UHDK_KEY_INSERT, ctrl, c.UHDK_EDIT_COPY },
|
||||||
|
.{ c.UHDK_KEY_INSERT, shift, c.UHDK_EDIT_PASTE },
|
||||||
|
.{ 'v', ctrl, c.UHDK_EDIT_PASTE },
|
||||||
|
.{ 'z', ctrl | shift, c.UHDK_EDIT_REDO },
|
||||||
|
.{ c.UHDK_KEY_UP, ctrl | shift, c.UHDK_EDIT_PARAGRAPH_START },
|
||||||
|
.{ c.UHDK_KEY_DOWN, ctrl, c.UHDK_EDIT_PARAGRAPH_END },
|
||||||
|
.{ c.UHDK_KEY_PAGE_UP, shift, c.UHDK_EDIT_PAGE_UP },
|
||||||
|
.{ c.UHDK_KEY_PAGE_DOWN, 0, c.UHDK_EDIT_PAGE_DOWN },
|
||||||
|
.{ c.UHDK_KEY_TAB, shift, c.UHDK_EDIT_BACKTAB },
|
||||||
|
.{ c.UHDK_KEY_ENTER, shift, c.UHDK_EDIT_LINE_BREAK },
|
||||||
|
.{ c.UHDK_KEY_ESCAPE, 0, c.UHDK_EDIT_CANCEL },
|
||||||
|
.{ c.UHDK_KEY_TAB, ctrl, 0 },
|
||||||
|
.{ c.UHDK_KEY_PAGE_DOWN, ctrl, 0 },
|
||||||
|
.{ 'v', ctrl | shift, 0 },
|
||||||
|
};
|
||||||
|
for (cases) |item| {
|
||||||
|
try std.testing.expectEqual(item[2], c.z_edit_key(item[0], item[1]));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), c.z_edit_key(item[0], item[1] | c.UHDK_MOD_ALT));
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), c.z_edit_key(item[0], item[1] | c.UHDK_MOD_SUPER));
|
||||||
|
}
|
||||||
|
try std.testing.expect(c.z_edit_extends_selection(c.UHDK_EDIT_WORD_LEFT, shift) != 0);
|
||||||
|
try std.testing.expect(c.z_edit_extends_selection(c.UHDK_EDIT_PAGE_DOWN, shift) != 0);
|
||||||
|
for ([_]u32{ c.UHDK_EDIT_BACKTAB, c.UHDK_EDIT_LINE_BREAK, c.UHDK_EDIT_CUT, c.UHDK_EDIT_DELETE_WORD_FORWARD, c.UHDK_EDIT_REDO }) |command|
|
||||||
|
try std.testing.expectEqual(@as(c_int, 0), c.z_edit_extends_selection(command, shift));
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
#ifndef UHDK_PRIVATE_EDIT_KEYS_H
|
||||||
|
#define UHDK_PRIVATE_EDIT_KEYS_H
|
||||||
|
#include "uhdk.h"
|
||||||
|
|
||||||
|
/* Shared Windows/Wayland defaults. Alt (including AltGr) and Super belong to
|
||||||
|
* text/layout or desktop shortcuts, never to these Ctrl editing bindings. */
|
||||||
|
static inline uint32_t z_edit_key(uint32_t key, uint32_t mods) {
|
||||||
|
int ctrl = (mods & UHDK_MOD_CONTROL) != 0;
|
||||||
|
int shift = (mods & UHDK_MOD_SHIFT) != 0;
|
||||||
|
if (mods & (UHDK_MOD_ALT | UHDK_MOD_SUPER))
|
||||||
|
return 0;
|
||||||
|
if (ctrl) {
|
||||||
|
switch (key) {
|
||||||
|
case 'a':
|
||||||
|
case 'A':
|
||||||
|
return shift ? 0 : UHDK_EDIT_SELECT_ALL;
|
||||||
|
case 'c':
|
||||||
|
case 'C':
|
||||||
|
return shift ? 0 : UHDK_EDIT_COPY;
|
||||||
|
case 'x':
|
||||||
|
case 'X':
|
||||||
|
return shift ? 0 : UHDK_EDIT_CUT;
|
||||||
|
case 'v':
|
||||||
|
case 'V':
|
||||||
|
return shift ? 0 : UHDK_EDIT_PASTE;
|
||||||
|
case 'z':
|
||||||
|
case 'Z':
|
||||||
|
return shift ? UHDK_EDIT_REDO : UHDK_EDIT_UNDO;
|
||||||
|
case 'y':
|
||||||
|
case 'Y':
|
||||||
|
return shift ? 0 : UHDK_EDIT_REDO;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (key) {
|
||||||
|
case UHDK_KEY_BACKSPACE:
|
||||||
|
return ctrl ? UHDK_EDIT_DELETE_WORD_BACKWARD : UHDK_EDIT_DELETE_BACKWARD;
|
||||||
|
case UHDK_KEY_DELETE:
|
||||||
|
return ctrl ? UHDK_EDIT_DELETE_WORD_FORWARD
|
||||||
|
: shift ? UHDK_EDIT_CUT
|
||||||
|
: UHDK_EDIT_DELETE_FORWARD;
|
||||||
|
case UHDK_KEY_INSERT:
|
||||||
|
return ctrl && !shift ? UHDK_EDIT_COPY : shift && !ctrl ? UHDK_EDIT_PASTE : 0;
|
||||||
|
case UHDK_KEY_LEFT:
|
||||||
|
return ctrl ? UHDK_EDIT_WORD_LEFT : UHDK_EDIT_MOVE_LEFT;
|
||||||
|
case UHDK_KEY_RIGHT:
|
||||||
|
return ctrl ? UHDK_EDIT_WORD_RIGHT : UHDK_EDIT_MOVE_RIGHT;
|
||||||
|
case UHDK_KEY_UP:
|
||||||
|
return ctrl ? UHDK_EDIT_PARAGRAPH_START : UHDK_EDIT_MOVE_UP;
|
||||||
|
case UHDK_KEY_DOWN:
|
||||||
|
return ctrl ? UHDK_EDIT_PARAGRAPH_END : UHDK_EDIT_MOVE_DOWN;
|
||||||
|
case UHDK_KEY_HOME:
|
||||||
|
return ctrl ? UHDK_EDIT_DOCUMENT_START : UHDK_EDIT_LINE_START;
|
||||||
|
case UHDK_KEY_END:
|
||||||
|
return ctrl ? UHDK_EDIT_DOCUMENT_END : UHDK_EDIT_LINE_END;
|
||||||
|
case UHDK_KEY_PAGE_UP:
|
||||||
|
return ctrl ? 0 : UHDK_EDIT_PAGE_UP;
|
||||||
|
case UHDK_KEY_PAGE_DOWN:
|
||||||
|
return ctrl ? 0 : UHDK_EDIT_PAGE_DOWN;
|
||||||
|
case UHDK_KEY_ENTER:
|
||||||
|
return ctrl ? 0 : shift ? UHDK_EDIT_LINE_BREAK : UHDK_EDIT_NEWLINE;
|
||||||
|
case UHDK_KEY_TAB:
|
||||||
|
return ctrl ? 0 : shift ? UHDK_EDIT_BACKTAB : UHDK_EDIT_TAB;
|
||||||
|
case UHDK_KEY_ESCAPE:
|
||||||
|
return ctrl || shift ? 0 : UHDK_EDIT_CANCEL;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static inline int z_edit_extends_selection(uint32_t command, uint32_t mods) {
|
||||||
|
if (!(mods & UHDK_MOD_SHIFT))
|
||||||
|
return 0;
|
||||||
|
switch (command) {
|
||||||
|
case UHDK_EDIT_MOVE_LEFT:
|
||||||
|
case UHDK_EDIT_MOVE_RIGHT:
|
||||||
|
case UHDK_EDIT_MOVE_UP:
|
||||||
|
case UHDK_EDIT_MOVE_DOWN:
|
||||||
|
case UHDK_EDIT_MOVE_BACKWARD:
|
||||||
|
case UHDK_EDIT_MOVE_FORWARD:
|
||||||
|
case UHDK_EDIT_WORD_LEFT:
|
||||||
|
case UHDK_EDIT_WORD_RIGHT:
|
||||||
|
case UHDK_EDIT_WORD_BACKWARD:
|
||||||
|
case UHDK_EDIT_WORD_FORWARD:
|
||||||
|
case UHDK_EDIT_LINE_START:
|
||||||
|
case UHDK_EDIT_LINE_END:
|
||||||
|
case UHDK_EDIT_LINE_LEFT_END:
|
||||||
|
case UHDK_EDIT_LINE_RIGHT_END:
|
||||||
|
case UHDK_EDIT_DOCUMENT_START:
|
||||||
|
case UHDK_EDIT_DOCUMENT_END:
|
||||||
|
case UHDK_EDIT_PARAGRAPH_START:
|
||||||
|
case UHDK_EDIT_PARAGRAPH_END:
|
||||||
|
case UHDK_EDIT_PAGE_UP:
|
||||||
|
case UHDK_EDIT_PAGE_DOWN:
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -260,7 +260,7 @@ fn emitKey(value: Key) void {
|
||||||
Popup.key(event.data.key.logical_key, value.down, s.serial);
|
Popup.key(event.data.key.logical_key, value.down, s.serial);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value.down and !value.repeated and KdeAppMenu.shortcut(w, event.data.key.logical_key, value.modifiers, event.input_id)) {
|
if (value.down and !value.repeated and !w.text.composing and KdeAppMenu.shortcut(w, event.data.key.logical_key, value.modifiers, event.input_id)) {
|
||||||
s.menu_keys[value.code] = true;
|
s.menu_keys[value.code] = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,31 +48,9 @@ pub fn logical(sym: u32) u32 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
pub fn edit(key: u32, mods: u32) u32 {
|
pub fn edit(key: u32, mods: u32) u32 {
|
||||||
const ctrl = mods & n.UHDK_MOD_CONTROL != 0;
|
return n.z_edit_key(key, mods);
|
||||||
if (mods & (n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return 0;
|
|
||||||
if (ctrl) switch (key) {
|
|
||||||
'c', 'C' => return n.UHDK_EDIT_COPY,
|
|
||||||
'x', 'X' => return n.UHDK_EDIT_CUT,
|
|
||||||
'v', 'V' => return n.UHDK_EDIT_PASTE,
|
|
||||||
'a', 'A' => return n.UHDK_EDIT_SELECT_ALL,
|
|
||||||
'z', 'Z' => return if (mods & n.UHDK_MOD_SHIFT != 0) n.UHDK_EDIT_REDO else n.UHDK_EDIT_UNDO,
|
|
||||||
'y', 'Y' => return n.UHDK_EDIT_REDO,
|
|
||||||
else => {},
|
|
||||||
};
|
|
||||||
return switch (key) {
|
|
||||||
n.UHDK_KEY_BACKSPACE => n.UHDK_EDIT_DELETE_BACKWARD,
|
|
||||||
n.UHDK_KEY_DELETE => n.UHDK_EDIT_DELETE_FORWARD,
|
|
||||||
n.UHDK_KEY_LEFT => if (ctrl) n.UHDK_EDIT_WORD_LEFT else n.UHDK_EDIT_MOVE_LEFT,
|
|
||||||
n.UHDK_KEY_RIGHT => if (ctrl) n.UHDK_EDIT_WORD_RIGHT else n.UHDK_EDIT_MOVE_RIGHT,
|
|
||||||
n.UHDK_KEY_UP => n.UHDK_EDIT_MOVE_UP,
|
|
||||||
n.UHDK_KEY_DOWN => n.UHDK_EDIT_MOVE_DOWN,
|
|
||||||
n.UHDK_KEY_HOME => if (ctrl) n.UHDK_EDIT_DOCUMENT_START else n.UHDK_EDIT_LINE_START,
|
|
||||||
n.UHDK_KEY_END => if (ctrl) n.UHDK_EDIT_DOCUMENT_END else n.UHDK_EDIT_LINE_END,
|
|
||||||
n.UHDK_KEY_ENTER => if (ctrl) 0 else n.UHDK_EDIT_NEWLINE,
|
|
||||||
n.UHDK_KEY_TAB => if (ctrl) 0 else n.UHDK_EDIT_TAB,
|
|
||||||
else => 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "physical positions and editor shortcuts" {
|
test "physical positions and editor shortcuts" {
|
||||||
try std.testing.expectEqual(@as(u32, 4), physical(30));
|
try std.testing.expectEqual(@as(u32, 4), physical(30));
|
||||||
try std.testing.expectEqual(@as(u32, 29), physical(44));
|
try std.testing.expectEqual(@as(u32, 29), physical(44));
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,14 @@ const n = @import("native");
|
||||||
const native = @import("native.zig");
|
const native = @import("native.zig");
|
||||||
const runtime = @import("../runtime.zig");
|
const runtime = @import("../runtime.zig");
|
||||||
const Input = @import("Input.zig");
|
const Input = @import("Input.zig");
|
||||||
|
const Text = @import("Text.zig");
|
||||||
pub fn activate(window: *native.Window, item: *const n.uhdk_menu_item, input_id: u64) void {
|
pub fn activate(window: *native.Window, item: *const n.uhdk_menu_item, input_id: u64) void {
|
||||||
var event = std.mem.zeroes(n.z_event);
|
var event = std.mem.zeroes(n.z_event);
|
||||||
if (item.role == n.UHDK_MENU_ROLE_QUIT) {
|
if (item.role == n.UHDK_MENU_ROLE_QUIT) {
|
||||||
event.type = n.UHDK_EVENT_QUIT_REQUESTED;
|
event.type = n.UHDK_EVENT_QUIT_REQUESTED;
|
||||||
n.z_emit(runtime.toAppHandle(window.owner), &event);
|
n.z_emit(runtime.toAppHandle(window.owner), &event);
|
||||||
} else if (item.role >= n.UHDK_MENU_ROLE_COPY and item.role <= n.UHDK_MENU_ROLE_REDO) {
|
} else if (item.role >= n.UHDK_MENU_ROLE_COPY and item.role <= n.UHDK_MENU_ROLE_REDO) {
|
||||||
event.type = n.UHDK_EVENT_EDIT_COMMAND;
|
Text.editCommand(window, @as(u32, n.UHDK_EDIT_COPY) + item.role - n.UHDK_MENU_ROLE_COPY, 0, input_id);
|
||||||
event.input_id = input_id;
|
|
||||||
event.data.edit.command = @as(u32, n.UHDK_EDIT_COPY) + item.role - n.UHDK_MENU_ROLE_COPY;
|
|
||||||
window.text.last_edit = input_id;
|
|
||||||
Input.send(window, &event);
|
|
||||||
} else {
|
} else {
|
||||||
event.type = n.UHDK_EVENT_MENU_COMMAND;
|
event.type = n.UHDK_EVENT_MENU_COMMAND;
|
||||||
event.data.menu.command_id = item.command_id;
|
event.data.menu.command_id = item.command_id;
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,11 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
||||||
application's show/hide intent, not proof that the compositor displays it.
|
application's show/hide intent, not proof that the compositor displays it.
|
||||||
- Size/state commands acknowledge submission. Confirmed geometry and focus arrive
|
- Size/state commands acknowledge submission. Confirmed geometry and focus arrive
|
||||||
through metrics. Wayland cannot report minimized state, force focus, or directly
|
through metrics. Wayland cannot report minimized state, force focus, or directly
|
||||||
undo minimization. Activation requests use a launch token or a recent focused
|
undo minimization. Metrics expose known resizing and (xdg-shell v6+) suspension
|
||||||
|
observations. Suspension is a rendering hint, not minimized/occluded state;
|
||||||
|
those two observation bits remain unset. Advertised unsupported window-manager
|
||||||
|
requests complete with `UNSUPPORTED`; older protocol versions remain best effort.
|
||||||
|
Activation requests use a launch token or a recent focused
|
||||||
input serial; the compositor can decline to focus the window.
|
input serial; the compositor can decline to focus the window.
|
||||||
- Fractional scale uses fractional-scale-v1 plus viewporter; otherwise preferred
|
- Fractional scale uses fractional-scale-v1 plus viewporter; otherwise preferred
|
||||||
integer buffer scale is used when wl_surface v6 is available. Older compositors
|
integer buffer scale is used when wl_surface v6 is available. Older compositors
|
||||||
|
|
@ -123,7 +127,12 @@ NixOS with Plasma 6.5.6 and Mesa 25.2.6; other systems are not runtime-qualified
|
||||||
forwarded keys. IME batches retain their surrounding-text revision and UTF-8
|
forwarded keys. IME batches retain their surrounding-text revision and UTF-8
|
||||||
offsets until `done`. Surrounding text is cropped to the protocol's 4000-byte
|
offsets until `done`. Surrounding text is cropped to the protocol's 4000-byte
|
||||||
limit; selections too large to fit disable surrounding-text edits for that
|
limit; selections too large to fit disable surrounding-text edits for that
|
||||||
snapshot. Ctrl editing shortcuts produce native edit events.
|
snapshot. Ctrl editing shortcuts produce native edit events, including word
|
||||||
|
deletion, paragraph/page navigation, Backtab, Shift+Enter, and menu-independent
|
||||||
|
clipboard actions. Shift extends only movement; Alt/Super combinations are left
|
||||||
|
to text/layout or desktop handling. Document/selection-changing commands retire
|
||||||
|
old IME snapshot serials, and an acknowledged snapshot publishes the new basis.
|
||||||
|
Escape first cancels composition before becoming an application action.
|
||||||
- Pointer motion, buttons, wheel/touchpad frames and cursor-shape-v1 are supported.
|
- Pointer motion, buttons, wheel/touchpad frames and cursor-shape-v1 are supported.
|
||||||
Capture acknowledges Wayland's implicit button grab during a selection drag;
|
Capture acknowledges Wayland's implicit button grab during a selection drag;
|
||||||
requesting it without a held button/focused pointer returns `UNAVAILABLE`.
|
requesting it without a held button/focused pointer returns `UNAVAILABLE`.
|
||||||
|
|
@ -215,6 +224,8 @@ python3 src/linux/tests/startup.py zig-out/bin/linux-native-tests
|
||||||
python3 src/linux/tests/disconnect.py zig-out/bin/linux-native-tests
|
python3 src/linux/tests/disconnect.py zig-out/bin/linux-native-tests
|
||||||
# In the optional nix-shell, or with dbus-next/dbus-daemon installed:
|
# In the optional nix-shell, or with dbus-next/dbus-daemon installed:
|
||||||
python3 src/linux/tests/wayland_fixture.py zig-out/bin/linux-service-tests
|
python3 src/linux/tests/wayland_fixture.py zig-out/bin/linux-service-tests
|
||||||
|
python3 src/linux/tests/editing_fixture.py zig-out/bin/linux-service-tests
|
||||||
|
python3 src/linux/tests/window_state_fixture.py zig-out/bin/linux-service-tests
|
||||||
python3 src/linux/tests/portal_fixture.py zig-out/bin/linux-service-tests
|
python3 src/linux/tests/portal_fixture.py zig-out/bin/linux-service-tests
|
||||||
python3 src/linux/tests/menu_fixture.py zig-out/bin/linux-service-tests
|
python3 src/linux/tests/menu_fixture.py zig-out/bin/linux-service-tests
|
||||||
python3 src/linux/tests/popup_fixture.py zig-out/bin/linux-service-tests
|
python3 src/linux/tests/popup_fixture.py zig-out/bin/linux-service-tests
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,10 @@ fn publish(w: *native.Window) u32 {
|
||||||
const s = Input.state();
|
const s = Input.state();
|
||||||
const t = &w.text;
|
const t = &w.text;
|
||||||
if (s.text_input == null or s.text_focus != w or !t.enabled) return abi.OK;
|
if (s.text_input == null or s.text_focus != w or !t.enabled) return abi.OK;
|
||||||
|
if (t.snapshot.acknowledged_input_id < t.last_edit) {
|
||||||
|
t.dirty = true;
|
||||||
|
return abi.OK;
|
||||||
|
}
|
||||||
// Bound unacknowledged snapshots without discarding snapshots a late IME
|
// Bound unacknowledged snapshots without discarding snapshots a late IME
|
||||||
// batch may still refer to. Keep the newest application state for later.
|
// batch may still refer to. Keep the newest application state for later.
|
||||||
if (t.sent_count >= 32) {
|
if (t.sent_count >= 32) {
|
||||||
|
|
@ -277,20 +281,41 @@ pub fn deinit(w: *native.Window) void {
|
||||||
forget(w);
|
forget(w);
|
||||||
w.owner.gpa.free(w.text.surrounding);
|
w.owner.gpa.free(w.text.surrounding);
|
||||||
}
|
}
|
||||||
|
pub fn editCommand(w: *native.Window, command: u32, modifiers: u32, input_id: u64) void {
|
||||||
|
if (w.text.ime_composing) return;
|
||||||
|
// A mutation or movement makes old surrounding-text ranges unsafe. Copy
|
||||||
|
// leaves the document/selection untouched, so delayed IME batches may retain
|
||||||
|
// their previously published basis. New state is published only on app ack.
|
||||||
|
if (command != n.UHDK_EDIT_COPY and command != n.UHDK_EDIT_CANCEL) {
|
||||||
|
clearPending(w);
|
||||||
|
clearSent(w);
|
||||||
|
}
|
||||||
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
event.type = n.UHDK_EVENT_EDIT_COMMAND;
|
||||||
|
event.input_id = input_id;
|
||||||
|
event.data.edit = .{ .command = command, .extend_selection = @intCast(n.z_edit_extends_selection(command, modifiers)) };
|
||||||
|
w.text.last_edit = input_id;
|
||||||
|
Input.send(w, &event);
|
||||||
|
}
|
||||||
pub fn key(w: *native.Window, sym: u32, logical: u32, input_id: u64, modifiers: u32, plain: []const u8) void {
|
pub fn key(w: *native.Window, sym: u32, logical: u32, input_id: u64, modifiers: u32, plain: []const u8) void {
|
||||||
const s = Input.state();
|
const s = Input.state();
|
||||||
const t = &w.text;
|
const t = &w.text;
|
||||||
if (t.ime_composing or s.state == null) return;
|
if (s.state == null) return;
|
||||||
|
if (t.ime_composing) {
|
||||||
|
if (Keyboard.edit(logical, modifiers) == n.UHDK_EDIT_CANCEL) cancel(w);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const edit = Keyboard.edit(logical, modifiers);
|
const edit = Keyboard.edit(logical, modifiers);
|
||||||
if (edit != 0) {
|
if (edit != 0) {
|
||||||
|
// Escape cancels an XKB compose sequence before becoming an app action.
|
||||||
|
if (edit == n.UHDK_EDIT_CANCEL and t.composing) {
|
||||||
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
endAt(w, input_id);
|
endAt(w, input_id);
|
||||||
var e = std.mem.zeroes(n.z_event);
|
return;
|
||||||
e.type = n.UHDK_EVENT_EDIT_COMMAND;
|
}
|
||||||
e.input_id = input_id;
|
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
||||||
e.data.edit = .{ .command = edit, .extend_selection = @intFromBool(modifiers & n.UHDK_MOD_SHIFT != 0) };
|
endAt(w, input_id);
|
||||||
t.last_edit = input_id;
|
editCommand(w, edit, modifiers, input_id);
|
||||||
Input.send(w, &e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (modifiers & (n.UHDK_MOD_CONTROL | n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return;
|
if (modifiers & (n.UHDK_MOD_CONTROL | n.UHDK_MOD_ALT | n.UHDK_MOD_SUPER) != 0) return;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
//! xdg-shell state values from protocols/xml/xdg-shell.xml. Kept separate from
|
||||||
|
//! Wayland objects so version/unknown-state behavior can be tested on any host.
|
||||||
|
const std = @import("std");
|
||||||
|
const n = @import("native");
|
||||||
|
|
||||||
|
pub const Observation = struct {
|
||||||
|
geometry: u32 = n.UHDK_WINDOW_NORMAL,
|
||||||
|
focused: bool = false,
|
||||||
|
resizing: bool = false,
|
||||||
|
suspended: bool = false,
|
||||||
|
known: u32 = n.UHDK_WINDOW_OBSERVE_RESIZING,
|
||||||
|
};
|
||||||
|
pub fn observe(version: u32, states: []const u32) Observation {
|
||||||
|
var result: Observation = .{};
|
||||||
|
if (version >= 6) result.known |= n.UHDK_WINDOW_OBSERVE_SUSPENDED;
|
||||||
|
for (states) |state| switch (state) {
|
||||||
|
1 => if (result.geometry != n.UHDK_WINDOW_FULLSCREEN) {
|
||||||
|
result.geometry = n.UHDK_WINDOW_MAXIMIZED;
|
||||||
|
},
|
||||||
|
2 => result.geometry = n.UHDK_WINDOW_FULLSCREEN,
|
||||||
|
3 => result.resizing = true,
|
||||||
|
4 => result.focused = true,
|
||||||
|
9 => result.suspended = version >= 6,
|
||||||
|
else => {},
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
pub const Capabilities = struct {
|
||||||
|
// Before v5 there is no advertisement; preserve best-effort requests.
|
||||||
|
minimize: bool = true,
|
||||||
|
maximize: bool = true,
|
||||||
|
fullscreen: bool = true,
|
||||||
|
|
||||||
|
pub fn announced(values: []const u32) Capabilities {
|
||||||
|
var result: Capabilities = .{ .minimize = false, .maximize = false, .fullscreen = false };
|
||||||
|
for (values) |value| switch (value) {
|
||||||
|
2 => result.maximize = true,
|
||||||
|
3 => result.fullscreen = true,
|
||||||
|
4 => result.minimize = true,
|
||||||
|
else => {},
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
test "Wayland observations do not invent minimization or occlusion" {
|
||||||
|
const old = observe(5, &.{ 1, 2, 3, 4, 9, 999 });
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_WINDOW_FULLSCREEN), old.geometry);
|
||||||
|
try std.testing.expect(old.focused and old.resizing and !old.suspended);
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_WINDOW_OBSERVE_RESIZING), old.known);
|
||||||
|
const modern = observe(6, &.{ 9, 2, 1 });
|
||||||
|
try std.testing.expect(modern.suspended and !modern.resizing);
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_WINDOW_FULLSCREEN), modern.geometry);
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_WINDOW_OBSERVE_RESIZING | n.UHDK_WINDOW_OBSERVE_SUSPENDED), modern.known);
|
||||||
|
const resumed = observe(6, &.{});
|
||||||
|
try std.testing.expect(!resumed.suspended and !resumed.focused and !resumed.resizing);
|
||||||
|
try std.testing.expectEqual(@as(u32, n.UHDK_WINDOW_NORMAL), resumed.geometry);
|
||||||
|
const caps = Capabilities.announced(&.{ 2, 999 });
|
||||||
|
try std.testing.expect(caps.maximize and !caps.minimize and !caps.fullscreen);
|
||||||
|
try std.testing.expect(!Capabilities.announced(&.{}).maximize);
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ const Application = @import("../Application.zig");
|
||||||
const Egl = @import("Egl.zig");
|
const Egl = @import("Egl.zig");
|
||||||
const Input = @import("Input.zig");
|
const Input = @import("Input.zig");
|
||||||
const Desktop = @import("Desktop.zig");
|
const Desktop = @import("Desktop.zig");
|
||||||
|
const WindowState = @import("WindowState.zig");
|
||||||
const Portal = @import("Portal.zig");
|
const Portal = @import("Portal.zig");
|
||||||
const Watch = @import("Watch.zig");
|
const Watch = @import("Watch.zig");
|
||||||
const Data = @import("Data.zig");
|
const Data = @import("Data.zig");
|
||||||
|
|
@ -44,6 +45,8 @@ pub const Window = struct {
|
||||||
scale: u32 = 120,
|
scale: u32 = 120,
|
||||||
focused: bool = false,
|
focused: bool = false,
|
||||||
state: u32 = n.UHDK_WINDOW_NORMAL,
|
state: u32 = n.UHDK_WINDOW_NORMAL,
|
||||||
|
observation: WindowState.Observation = .{},
|
||||||
|
wm_caps: WindowState.Capabilities = .{},
|
||||||
flags: u32,
|
flags: u32,
|
||||||
cursor: u32 = n.UHDK_CURSOR_ARROW,
|
cursor: u32 = n.UHDK_CURSOR_ARROW,
|
||||||
text: Text.State = .{},
|
text: Text.State = .{},
|
||||||
|
|
@ -358,20 +361,10 @@ fn topConfigure(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel, width: i32, heigh
|
||||||
const w = window(data);
|
const w = window(data);
|
||||||
if (width > 0) w.width = width;
|
if (width > 0) w.width = width;
|
||||||
if (height > 0) w.height = height;
|
if (height > 0) w.height = height;
|
||||||
w.focused = false;
|
const values: []const u32 = if (states.*.size == 0) &.{} else @as([*]const u32, @ptrCast(@alignCast(states.*.data)))[0 .. states.*.size / @sizeOf(u32)];
|
||||||
w.state = n.UHDK_WINDOW_NORMAL;
|
w.observation = WindowState.observe(c.xdg_toplevel_get_version(w.top), values);
|
||||||
if (states.*.size == 0) return;
|
w.focused = w.observation.focused;
|
||||||
const values: [*]const u32 = @ptrCast(@alignCast(states.*.data));
|
w.state = w.observation.geometry;
|
||||||
for (values[0 .. states.*.size / @sizeOf(u32)]) |value| {
|
|
||||||
switch (value) {
|
|
||||||
c.XDG_TOPLEVEL_STATE_ACTIVATED => w.focused = true,
|
|
||||||
c.XDG_TOPLEVEL_STATE_MAXIMIZED => if (w.state != n.UHDK_WINDOW_FULLSCREEN) {
|
|
||||||
w.state = n.UHDK_WINDOW_MAXIMIZED;
|
|
||||||
},
|
|
||||||
c.XDG_TOPLEVEL_STATE_FULLSCREEN => w.state = n.UHDK_WINDOW_FULLSCREEN,
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fn emit(w: *Window, kind: u32) void {
|
fn emit(w: *Window, kind: u32) void {
|
||||||
var event = std.mem.zeroes(n.z_event);
|
var event = std.mem.zeroes(n.z_event);
|
||||||
|
|
@ -383,7 +376,10 @@ fn topClose(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel) callconv(.c) void {
|
||||||
emit(window(data), n.UHDK_EVENT_WINDOW_CLOSE_REQUESTED);
|
emit(window(data), n.UHDK_EVENT_WINDOW_CLOSE_REQUESTED);
|
||||||
}
|
}
|
||||||
fn bounds(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: i32, _: i32) callconv(.c) void {}
|
fn bounds(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: i32, _: i32) callconv(.c) void {}
|
||||||
fn wmCaps(_: ?*anyopaque, _: ?*c.struct_xdg_toplevel, _: [*c]c.struct_wl_array) callconv(.c) void {}
|
fn wmCaps(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel, caps: [*c]c.struct_wl_array) callconv(.c) void {
|
||||||
|
const values: []const u32 = if (caps.*.size == 0) &.{} else @as([*]const u32, @ptrCast(@alignCast(caps.*.data)))[0 .. caps.*.size / @sizeOf(u32)];
|
||||||
|
window(data).wm_caps = WindowState.Capabilities.announced(values);
|
||||||
|
}
|
||||||
const top_listener: c.struct_xdg_toplevel_listener = .{ .configure = topConfigure, .close = topClose, .configure_bounds = bounds, .wm_capabilities = wmCaps };
|
const top_listener: c.struct_xdg_toplevel_listener = .{ .configure = topConfigure, .close = topClose, .configure_bounds = bounds, .wm_capabilities = wmCaps };
|
||||||
fn decorationConfigure(data: ?*anyopaque, _: ?*c.struct_zxdg_toplevel_decoration_v1, mode: u32) callconv(.c) void {
|
fn decorationConfigure(data: ?*anyopaque, _: ?*c.struct_zxdg_toplevel_decoration_v1, mode: u32) callconv(.c) void {
|
||||||
const w = window(data);
|
const w = window(data);
|
||||||
|
|
@ -454,7 +450,7 @@ fn apply(w: *Window) void {
|
||||||
w.configured = false;
|
w.configured = false;
|
||||||
}
|
}
|
||||||
w.visible = w.want_visible;
|
w.visible = w.want_visible;
|
||||||
w.metrics = .{ .struct_size = @sizeOf(n.uhdk_window_metrics), .framebuffer_width = pixel_width, .framebuffer_height = pixel_height, .logical_size = .{ .width = @floatFromInt(width), .height = @floatFromInt(height) }, .scale = @as(f32, @floatFromInt(w.scale)) / 120, .state = w.state, .focused = @intFromBool(w.focused), .visible = @intFromBool(w.visible) };
|
w.metrics = .{ .struct_size = @sizeOf(n.uhdk_window_metrics), .framebuffer_width = pixel_width, .framebuffer_height = pixel_height, .logical_size = .{ .width = @floatFromInt(width), .height = @floatFromInt(height) }, .scale = @as(f32, @floatFromInt(w.scale)) / 120, .state = w.state, .focused = @intFromBool(w.focused), .visible = @intFromBool(w.visible), .observations = w.observation.known, .resizing = @intFromBool(w.observation.resizing), .suspended = @intFromBool(w.observation.suspended), .minimized = 0, .occluded = 0 };
|
||||||
const metrics = w.metrics;
|
const metrics = w.metrics;
|
||||||
w.dirty = false;
|
w.dirty = false;
|
||||||
w.mutex.unlock(w.owner.io);
|
w.mutex.unlock(w.owner.io);
|
||||||
|
|
@ -602,15 +598,22 @@ export fn z_native_execute(_: ?*n.uhdk_app, raw: [*c]const n.z_command, id: u64,
|
||||||
w.dirty = true;
|
w.dirty = true;
|
||||||
},
|
},
|
||||||
n.Z_STATE => switch (cmd.number) {
|
n.Z_STATE => switch (cmd.number) {
|
||||||
n.UHDK_WINDOW_MINIMIZED => c.xdg_toplevel_set_minimized(w.top),
|
n.UHDK_WINDOW_MINIMIZED => {
|
||||||
|
if (!w.wm_caps.minimize) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
c.xdg_toplevel_set_minimized(w.top);
|
||||||
|
},
|
||||||
n.UHDK_WINDOW_MAXIMIZED => {
|
n.UHDK_WINDOW_MAXIMIZED => {
|
||||||
c.xdg_toplevel_unset_fullscreen(w.top);
|
if (!w.wm_caps.maximize) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
if (w.wm_caps.fullscreen) c.xdg_toplevel_unset_fullscreen(w.top);
|
||||||
c.xdg_toplevel_set_maximized(w.top);
|
c.xdg_toplevel_set_maximized(w.top);
|
||||||
},
|
},
|
||||||
n.UHDK_WINDOW_FULLSCREEN => c.xdg_toplevel_set_fullscreen(w.top, null),
|
n.UHDK_WINDOW_FULLSCREEN => {
|
||||||
|
if (!w.wm_caps.fullscreen) return n.UHDK_STATUS_UNSUPPORTED;
|
||||||
|
c.xdg_toplevel_set_fullscreen(w.top, null);
|
||||||
|
},
|
||||||
else => {
|
else => {
|
||||||
c.xdg_toplevel_unset_fullscreen(w.top);
|
if (w.wm_caps.fullscreen) c.xdg_toplevel_unset_fullscreen(w.top);
|
||||||
c.xdg_toplevel_unset_maximized(w.top);
|
if (w.wm_caps.maximize) c.xdg_toplevel_unset_maximized(w.top);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
n.Z_VISIBLE => {
|
n.Z_VISIBLE => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Forward isolated XKB keys to verify edit defaults without an application menu."""
|
||||||
|
import sys
|
||||||
|
from wayland_fixture import Compositor, run
|
||||||
|
|
||||||
|
|
||||||
|
class EditingCompositor(Compositor):
|
||||||
|
# evdev code, XKB depressed-mask (Shift=1, Control=4, Mod1=8).
|
||||||
|
gestures = [(14, 4), (111, 4), (103, 4), (108, 5), (104, 1), (109, 0),
|
||||||
|
(15, 1), (28, 1), (111, 1), (110, 4), (110, 1), (46, 4),
|
||||||
|
(44, 5), (14, 12), (15, 4)]
|
||||||
|
|
||||||
|
def timers(self):
|
||||||
|
# The fixture tests forwarded keys, not compositor-owned IME input.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def request(self, obj, iface, name, args):
|
||||||
|
if iface == 'xdg_toplevel' and name == 'set_title' and args[0].startswith('edit:'):
|
||||||
|
index = int(args[0].split(':')[1])
|
||||||
|
code, mask = self.gestures[index]
|
||||||
|
keyboard = self.ids['wl_keyboard']
|
||||||
|
if index == 0:
|
||||||
|
self.event(keyboard, 'enter', 200, next(iter(self.surfaces)), b'')
|
||||||
|
self.event(keyboard, 'modifiers', 201, mask, 0, 0, 0)
|
||||||
|
self.event(keyboard, 'key', 202, index * 2, code, 1)
|
||||||
|
self.event(keyboard, 'key', 203, index * 2 + 1, code, 0)
|
||||||
|
self.event(keyboard, 'modifiers', 204, 0, 0, 0, 0)
|
||||||
|
super().request(obj, iface, name, args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run(sys.argv[1], 'editing', compositor_type=EditingCompositor)
|
||||||
|
|
@ -212,6 +212,129 @@ static void portals(uhdk_app *app) {
|
||||||
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
|
assert(wait_result(app, id).status == UHDK_STATUS_CANCELLED);
|
||||||
puts("portal results/cancellation/parent/restart: PASS");
|
puts("portal results/cancellation/parent/restart: PASS");
|
||||||
}
|
}
|
||||||
|
static void window_observations(uhdk_app *app, int version) {
|
||||||
|
uhdk_window *w = window(app);
|
||||||
|
uhdk_window_metrics metrics = {0};
|
||||||
|
metrics.struct_size = sizeof(metrics);
|
||||||
|
OK(uhdk_window_get_metrics(w, &metrics));
|
||||||
|
uint32_t known =
|
||||||
|
UHDK_WINDOW_OBSERVE_RESIZING | (version >= 6 ? UHDK_WINDOW_OBSERVE_SUSPENDED : 0);
|
||||||
|
assert(metrics.observations == known);
|
||||||
|
assert(!metrics.minimized && !metrics.occluded && !metrics.suspended && !metrics.resizing);
|
||||||
|
uint64_t request;
|
||||||
|
OK(uhdk_window_set_state_async(w, UHDK_WINDOW_MINIMIZED, &request));
|
||||||
|
assert(wait_result(app, request).status == UHDK_STATUS_UNSUPPORTED);
|
||||||
|
OK(uhdk_window_set_state_async(w, UHDK_WINDOW_FULLSCREEN, &request));
|
||||||
|
assert(wait_result(app, request).status == UHDK_STATUS_UNSUPPORTED);
|
||||||
|
for (int phase = 0; phase < 2; ++phase) {
|
||||||
|
OK(uhdk_window_set_title_async(
|
||||||
|
w, text(phase ? "fixture:resume" : "fixture:suspend"), &request
|
||||||
|
));
|
||||||
|
int matched = 0, completed = 0;
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (uhdk_app_get_time_ns(app) < end && !(matched && completed)) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 50000000, &events));
|
||||||
|
for (uint64_t i = 0; i < events.len; ++i) {
|
||||||
|
if (events.ptr[i].type == UHDK_EVENT_WINDOW_METRICS) {
|
||||||
|
const uhdk_window_metrics *m = events.ptr[i].data;
|
||||||
|
assert(m->observations == known);
|
||||||
|
assert(!m->minimized && !m->occluded);
|
||||||
|
if (m->state == (phase ? UHDK_WINDOW_NORMAL : UHDK_WINDOW_FULLSCREEN) &&
|
||||||
|
!!m->resizing == !phase && !!m->focused == !phase &&
|
||||||
|
!!m->suspended == (!phase && version >= 6))
|
||||||
|
matched = 1;
|
||||||
|
} else if (events.ptr[i].type == UHDK_EVENT_REQUEST_DONE) {
|
||||||
|
const uhdk_request_result *r = events.ptr[i].data;
|
||||||
|
if (r->id == request) {
|
||||||
|
OK(r->status);
|
||||||
|
completed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(matched && completed);
|
||||||
|
}
|
||||||
|
OK(uhdk_window_set_state_async(w, UHDK_WINDOW_MINIMIZED, &request));
|
||||||
|
OK(wait_result(app, request).status);
|
||||||
|
OK(uhdk_window_get_metrics(w, &metrics));
|
||||||
|
assert(!(metrics.observations & UHDK_WINDOW_OBSERVE_MINIMIZED));
|
||||||
|
assert(metrics.state == UHDK_WINDOW_NORMAL); // Submission is not observation.
|
||||||
|
uhdk_window_destroy(w);
|
||||||
|
puts("Wayland versioned window observations/capabilities: PASS");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void editing(uhdk_app *app) {
|
||||||
|
uhdk_window *w = window(app);
|
||||||
|
uint64_t request;
|
||||||
|
OK(uhdk_window_set_text_input_async(w, 1, &request));
|
||||||
|
OK(wait_result(app, request).status);
|
||||||
|
const struct {
|
||||||
|
uint32_t command;
|
||||||
|
int extend;
|
||||||
|
} cases[] = {
|
||||||
|
{UHDK_EDIT_DELETE_WORD_BACKWARD, 0},
|
||||||
|
{UHDK_EDIT_DELETE_WORD_FORWARD, 0},
|
||||||
|
{UHDK_EDIT_PARAGRAPH_START, 0},
|
||||||
|
{UHDK_EDIT_PARAGRAPH_END, 1},
|
||||||
|
{UHDK_EDIT_PAGE_UP, 1},
|
||||||
|
{UHDK_EDIT_PAGE_DOWN, 0},
|
||||||
|
{UHDK_EDIT_BACKTAB, 0},
|
||||||
|
{UHDK_EDIT_LINE_BREAK, 0},
|
||||||
|
{UHDK_EDIT_CUT, 0},
|
||||||
|
{UHDK_EDIT_COPY, 0},
|
||||||
|
{UHDK_EDIT_PASTE, 0},
|
||||||
|
{UHDK_EDIT_COPY, 0},
|
||||||
|
{UHDK_EDIT_REDO, 0},
|
||||||
|
{0, 0},
|
||||||
|
{0, 0}
|
||||||
|
};
|
||||||
|
uint64_t acknowledged = 0;
|
||||||
|
for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) {
|
||||||
|
uhdk_text_state state = {0};
|
||||||
|
state.struct_size = sizeof(state);
|
||||||
|
state.surrounding_text = text("one two");
|
||||||
|
state.selection_start = 7;
|
||||||
|
state.revision = i + 1;
|
||||||
|
state.acknowledged_input_id = acknowledged;
|
||||||
|
OK(uhdk_window_set_text_state(w, &state));
|
||||||
|
char title[32];
|
||||||
|
snprintf(title, sizeof(title), "edit:%u", i);
|
||||||
|
OK(uhdk_window_set_title_async(w, text(title), &request));
|
||||||
|
int released = 0, completed = 0;
|
||||||
|
unsigned commands = 0;
|
||||||
|
uint64_t end = deadline(app);
|
||||||
|
while (uhdk_app_get_time_ns(app) < end && !(released && completed)) {
|
||||||
|
uhdk_event_list events;
|
||||||
|
OK(uhdk_app_poll_events(app, 50000000, &events));
|
||||||
|
for (uint64_t j = 0; j < events.len; ++j) {
|
||||||
|
const uhdk_event *event = &events.ptr[j];
|
||||||
|
assert(event->type != UHDK_EVENT_TEXT_COMMIT);
|
||||||
|
if (event->type == UHDK_EVENT_KEY) {
|
||||||
|
const uhdk_key_event *key = event->data;
|
||||||
|
if (!key->pressed)
|
||||||
|
released = 1;
|
||||||
|
} else if (event->type == UHDK_EVENT_EDIT_COMMAND) {
|
||||||
|
const uhdk_edit_event *edit = event->data;
|
||||||
|
assert(edit->command == cases[i].command);
|
||||||
|
assert(!!edit->extend_selection == cases[i].extend);
|
||||||
|
acknowledged = event->input_id;
|
||||||
|
++commands;
|
||||||
|
} else if (event->type == UHDK_EVENT_REQUEST_DONE) {
|
||||||
|
const uhdk_request_result *r = event->data;
|
||||||
|
if (r->id == request) {
|
||||||
|
OK(r->status);
|
||||||
|
completed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(released && completed && commands == (cases[i].command ? 1u : 0u));
|
||||||
|
}
|
||||||
|
uhdk_window_destroy(w);
|
||||||
|
puts("Wayland editing defaults/selection/AltGr/no duplicate text: PASS");
|
||||||
|
}
|
||||||
|
|
||||||
static void input(uhdk_app *app) {
|
static void input(uhdk_app *app) {
|
||||||
uhdk_window *w = window(app);
|
uhdk_window *w = window(app);
|
||||||
uint64_t id;
|
uint64_t id;
|
||||||
|
|
@ -379,6 +502,14 @@ static void run(uhdk_app *app, void *mode) {
|
||||||
menus(app);
|
menus(app);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (strncmp(mode, "window-state-v", 14) == 0) {
|
||||||
|
window_observations(app, atoi(mode + 14));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (strcmp(mode, "editing") == 0) {
|
||||||
|
editing(app);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (strcmp(mode, "input") == 0)
|
if (strcmp(mode, "input") == 0)
|
||||||
input(app);
|
input(app);
|
||||||
else if (strcmp(mode, "portal") == 0)
|
else if (strcmp(mode, "portal") == 0)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Exercise xdg-shell versioned observations and capability changes in isolation."""
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
from wayland_fixture import Compositor, run
|
||||||
|
|
||||||
|
|
||||||
|
class WindowStateCompositor(Compositor):
|
||||||
|
version = 6
|
||||||
|
|
||||||
|
def __init__(self, sock, inject=False):
|
||||||
|
super().__init__(sock, inject)
|
||||||
|
self.top = None
|
||||||
|
self.states = []
|
||||||
|
self.caps = [2] # Maximize only; minimize/fullscreen must be rejected.
|
||||||
|
self.minimized = False
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.event(self.top, 'wm_capabilities', struct.pack('=' + 'I' * len(self.caps), *self.caps))
|
||||||
|
self.event(self.top, 'configure', 320, 240, struct.pack('=' + 'I' * len(self.states), *self.states))
|
||||||
|
self.serial += 1
|
||||||
|
self.event(next(iter(self.surfaces.values())), 'configure', self.serial)
|
||||||
|
|
||||||
|
def request(self, obj, iface, name, args):
|
||||||
|
if iface == 'xdg_surface' and name == 'get_toplevel':
|
||||||
|
self.top = args[0]
|
||||||
|
elif iface == 'wl_surface' and name == 'commit' and obj in self.surfaces:
|
||||||
|
self.configure()
|
||||||
|
return
|
||||||
|
elif iface == 'xdg_toplevel' and name == 'set_title':
|
||||||
|
if args[0] == 'fixture:suspend':
|
||||||
|
self.states = [2, 3, 4] + ([9] if self.version >= 6 else [])
|
||||||
|
self.configure()
|
||||||
|
elif args[0] == 'fixture:resume':
|
||||||
|
self.states = []
|
||||||
|
self.caps = [2, 3, 4]
|
||||||
|
self.configure()
|
||||||
|
elif iface == 'xdg_toplevel' and name == 'set_minimized':
|
||||||
|
assert 4 in self.caps, 'client sent an unavailable minimize request'
|
||||||
|
self.minimized = True
|
||||||
|
elif iface == 'xdg_toplevel' and name == 'destroy':
|
||||||
|
assert self.minimized, 'accepted minimize request was not submitted'
|
||||||
|
elif iface == 'xdg_toplevel' and name == 'set_fullscreen':
|
||||||
|
assert 3 in self.caps, 'client sent an unavailable fullscreen request'
|
||||||
|
super().request(obj, iface, name, args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
for version in (5, 6):
|
||||||
|
class Versioned(WindowStateCompositor):
|
||||||
|
globals = [(name, version if name == 'xdg_wm_base' else v)
|
||||||
|
for name, v in Compositor.globals]
|
||||||
|
Versioned.version = version
|
||||||
|
run(sys.argv[1], 'window-state-v' + str(version), compositor_type=Versioned)
|
||||||
|
|
@ -56,6 +56,11 @@ static uhdk_modifier_flags mods(NSEventModifierFlags f) {
|
||||||
@property(nonatomic, strong) ZGL *gl;
|
@property(nonatomic, strong) ZGL *gl;
|
||||||
@property(nonatomic) uhdk_menu *menu;
|
@property(nonatomic) uhdk_menu *menu;
|
||||||
@property(nonatomic) uint32_t flags;
|
@property(nonatomic) uint32_t flags;
|
||||||
|
@property(nonatomic) BOOL resizing;
|
||||||
|
@property(nonatomic) BOOL fullscreenTransition;
|
||||||
|
@property(nonatomic) BOOL hasPendingState;
|
||||||
|
@property(nonatomic) uint32_t pendingState;
|
||||||
|
- (void)applyPendingState;
|
||||||
- (void)metrics;
|
- (void)metrics;
|
||||||
@end
|
@end
|
||||||
@interface ZView : NSView <NSTextInputClient>
|
@interface ZView : NSView <NSTextInputClient>
|
||||||
|
|
@ -75,6 +80,8 @@ static uhdk_modifier_flags mods(NSEventModifierFlags f) {
|
||||||
@property(nonatomic) uint64_t currentInput;
|
@property(nonatomic) uint64_t currentInput;
|
||||||
@property(nonatomic) uint64_t lastEdit;
|
@property(nonatomic) uint64_t lastEdit;
|
||||||
@property(nonatomic) uhdk_rect caret;
|
@property(nonatomic) uhdk_rect caret;
|
||||||
|
@property(nonatomic) BOOL awaitingSnapshot;
|
||||||
|
@property(nonatomic, strong) NSMutableArray<void (^)(void)> *deferredInput;
|
||||||
- (void)emit:(z_event *)event;
|
- (void)emit:(z_event *)event;
|
||||||
- (void)cancelMarked;
|
- (void)cancelMarked;
|
||||||
- (void)applyState:(const uhdk_text_state *)state;
|
- (void)applyState:(const uhdk_text_state *)state;
|
||||||
|
|
@ -183,6 +190,7 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
_marked = NSMakeRange(NSNotFound, 0);
|
_marked = NSMakeRange(NSNotFound, 0);
|
||||||
_cursor = NSCursor.arrowCursor;
|
_cursor = NSCursor.arrowCursor;
|
||||||
_pressedModifiers = [NSMutableIndexSet indexSet];
|
_pressedModifiers = [NSMutableIndexSet indexSet];
|
||||||
|
_deferredInput = [NSMutableArray array];
|
||||||
self.wantsBestResolutionOpenGLSurface = YES;
|
self.wantsBestResolutionOpenGLSurface = YES;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
|
|
@ -211,6 +219,16 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
[super setFrameSize:size];
|
[super setFrameSize:size];
|
||||||
[self.owner metrics];
|
[self.owner metrics];
|
||||||
}
|
}
|
||||||
|
- (void)viewWillStartLiveResize {
|
||||||
|
[super viewWillStartLiveResize];
|
||||||
|
self.owner.resizing = YES;
|
||||||
|
[self.owner metrics];
|
||||||
|
}
|
||||||
|
- (void)viewDidEndLiveResize {
|
||||||
|
[super viewDidEndLiveResize];
|
||||||
|
self.owner.resizing = NO;
|
||||||
|
[self.owner metrics];
|
||||||
|
}
|
||||||
- (void)viewDidChangeBackingProperties {
|
- (void)viewDidChangeBackingProperties {
|
||||||
[super viewDidChangeBackingProperties];
|
[super viewDidChangeBackingProperties];
|
||||||
[self.owner metrics];
|
[self.owner metrics];
|
||||||
|
|
@ -235,6 +253,13 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
[self.cursor set];
|
[self.cursor set];
|
||||||
}
|
}
|
||||||
- (void)key:(NSEvent *)event pressed:(BOOL)pressed {
|
- (void)key:(NSEvent *)event pressed:(BOOL)pressed {
|
||||||
|
if (self.textEnabled && self.awaitingSnapshot) {
|
||||||
|
__weak ZView *view = self;
|
||||||
|
[self.deferredInput addObject:^{
|
||||||
|
[view key:event pressed:pressed];
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.currentInput = ++self.nextInput;
|
self.currentInput = ++self.nextInput;
|
||||||
z_event e = {.type = UHDK_EVENT_KEY};
|
z_event e = {.type = UHDK_EVENT_KEY};
|
||||||
e.data.key.physical_key = physical(event.keyCode);
|
e.data.key.physical_key = physical(event.keyCode);
|
||||||
|
|
@ -243,10 +268,55 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
e.data.key.pressed = pressed;
|
e.data.key.pressed = pressed;
|
||||||
e.data.key.repeat = event.isARepeat;
|
e.data.key.repeat = event.isARepeat;
|
||||||
[self emit:&e];
|
[self emit:&e];
|
||||||
if (pressed && self.textEnabled && !(event.modifierFlags & NSEventModifierFlagCommand))
|
if (pressed && self.textEnabled)
|
||||||
[self interpretKeyEvents:@[ event ]];
|
[self interpretKeyEvents:@[ event ]];
|
||||||
self.currentInput = 0;
|
self.currentInput = 0;
|
||||||
}
|
}
|
||||||
|
// The menu gets first refusal. Provide standard editing even without an Edit menu.
|
||||||
|
- (BOOL)performKeyEquivalent:(NSEvent *)event {
|
||||||
|
if (!self.textEnabled || !(event.modifierFlags & NSEventModifierFlagCommand))
|
||||||
|
return [super performKeyEquivalent:event];
|
||||||
|
if ([NSApp.mainMenu performKeyEquivalent:event])
|
||||||
|
return YES;
|
||||||
|
uint32_t key = logical(event);
|
||||||
|
NSEventModifierFlags flags =
|
||||||
|
event.modifierFlags & NSEventModifierFlagDeviceIndependentFlagsMask;
|
||||||
|
BOOL plain = !(flags & (NSEventModifierFlagOption | NSEventModifierFlagControl));
|
||||||
|
SEL selector = NULL;
|
||||||
|
if (plain) {
|
||||||
|
switch (key) {
|
||||||
|
case 'c':
|
||||||
|
if (!(flags & NSEventModifierFlagShift))
|
||||||
|
selector = @selector(copy:);
|
||||||
|
break;
|
||||||
|
case 'x':
|
||||||
|
if (!(flags & NSEventModifierFlagShift))
|
||||||
|
selector = @selector(cut:);
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
if (!(flags & NSEventModifierFlagShift))
|
||||||
|
selector = @selector(paste:);
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
if (!(flags & NSEventModifierFlagShift))
|
||||||
|
selector = @selector(selectAll:);
|
||||||
|
break;
|
||||||
|
case 'z':
|
||||||
|
selector = flags & NSEventModifierFlagShift ? @selector(redo:) : @selector(undo:);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selector) {
|
||||||
|
[self doCommandBySelector:selector];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
if (key == UHDK_KEY_LEFT || key == UHDK_KEY_RIGHT || key == UHDK_KEY_UP ||
|
||||||
|
key == UHDK_KEY_DOWN || key == UHDK_KEY_BACKSPACE || key == UHDK_KEY_DELETE) {
|
||||||
|
[self key:event pressed:YES];
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
return [super performKeyEquivalent:event];
|
||||||
|
}
|
||||||
- (void)keyDown:(NSEvent *)event {
|
- (void)keyDown:(NSEvent *)event {
|
||||||
[self key:event pressed:YES];
|
[self key:event pressed:YES];
|
||||||
}
|
}
|
||||||
|
|
@ -376,6 +446,12 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
end = utf16Offset(self.textBuffer, state->selection_start + state->selection_len);
|
end = utf16Offset(self.textBuffer, state->selection_start + state->selection_len);
|
||||||
self.selection = NSMakeRange(start, end - start);
|
self.selection = NSMakeRange(start, end - start);
|
||||||
[self.inputContext invalidateCharacterCoordinates];
|
[self.inputContext invalidateCharacterCoordinates];
|
||||||
|
self.awaitingSnapshot = NO;
|
||||||
|
while (!self.awaitingSnapshot && self.deferredInput.count) {
|
||||||
|
void (^input)(void) = self.deferredInput.firstObject;
|
||||||
|
[self.deferredInput removeObjectAtIndex:0];
|
||||||
|
input();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
- (void)emitText:(NSString *)s
|
- (void)emitText:(NSString *)s
|
||||||
type:(uint32_t)type
|
type:(uint32_t)type
|
||||||
|
|
@ -395,6 +471,14 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
[self emit:&e];
|
[self emit:&e];
|
||||||
}
|
}
|
||||||
- (void)insertText:(id)value replacementRange:(NSRange)range {
|
- (void)insertText:(id)value replacementRange:(NSRange)range {
|
||||||
|
if (self.awaitingSnapshot) {
|
||||||
|
__weak ZView *view = self;
|
||||||
|
id copy = [value copy];
|
||||||
|
[self.deferredInput addObject:^{
|
||||||
|
[view insertText:copy replacementRange:range];
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
BOOL ownInput = self.currentInput == 0;
|
BOOL ownInput = self.currentInput == 0;
|
||||||
if (ownInput)
|
if (ownInput)
|
||||||
self.currentInput = ++self.nextInput;
|
self.currentInput = ++self.nextInput;
|
||||||
|
|
@ -425,6 +509,14 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
self.currentInput = 0;
|
self.currentInput = 0;
|
||||||
}
|
}
|
||||||
- (void)setMarkedText:(id)value selectedRange:(NSRange)selected replacementRange:(NSRange)range {
|
- (void)setMarkedText:(id)value selectedRange:(NSRange)selected replacementRange:(NSRange)range {
|
||||||
|
if (self.awaitingSnapshot) {
|
||||||
|
__weak ZView *view = self;
|
||||||
|
id copy = [value copy];
|
||||||
|
[self.deferredInput addObject:^{
|
||||||
|
[view setMarkedText:copy selectedRange:selected replacementRange:range];
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
BOOL ownInput = self.currentInput == 0;
|
BOOL ownInput = self.currentInput == 0;
|
||||||
if (ownInput)
|
if (ownInput)
|
||||||
self.currentInput = ++self.nextInput;
|
self.currentInput = ++self.nextInput;
|
||||||
|
|
@ -477,14 +569,14 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
return self.marked;
|
return self.marked;
|
||||||
}
|
}
|
||||||
- (NSRange)selectedRange {
|
- (NSRange)selectedRange {
|
||||||
return self.selection;
|
return self.awaitingSnapshot ? NSMakeRange(NSNotFound, 0) : self.selection;
|
||||||
}
|
}
|
||||||
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
||||||
return @[];
|
return @[];
|
||||||
}
|
}
|
||||||
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)r
|
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)r
|
||||||
actualRange:(NSRangePointer)actual {
|
actualRange:(NSRangePointer)actual {
|
||||||
if (r.location == NSNotFound || r.location > self.textBuffer.length) {
|
if (self.awaitingSnapshot || r.location == NSNotFound || r.location > self.textBuffer.length) {
|
||||||
if (actual)
|
if (actual)
|
||||||
*actual = NSMakeRange(NSNotFound, 0);
|
*actual = NSMakeRange(NSNotFound, 0);
|
||||||
return nil;
|
return nil;
|
||||||
|
|
@ -501,33 +593,72 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actual {
|
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actual {
|
||||||
(void)range;
|
(void)range;
|
||||||
if (actual)
|
if (actual)
|
||||||
*actual = self.selection;
|
*actual = [self selectedRange];
|
||||||
NSRect rect = NSMakeRect(self.caret.x, self.caret.y, self.caret.width, self.caret.height);
|
NSRect rect = NSMakeRect(self.caret.x, self.caret.y, self.caret.width, self.caret.height);
|
||||||
return [self.window convertRectToScreen:[self convertRect:rect toView:nil]];
|
return [self.window convertRectToScreen:[self convertRect:rect toView:nil]];
|
||||||
}
|
}
|
||||||
- (void)doCommandBySelector:(SEL)selector {
|
- (void)doCommandBySelector:(SEL)selector {
|
||||||
|
if (!self.textEnabled)
|
||||||
|
return;
|
||||||
|
if (self.awaitingSnapshot) {
|
||||||
|
__weak ZView *view = self;
|
||||||
|
[self.deferredInput addObject:^{
|
||||||
|
[view doCommandBySelector:selector];
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ([self hasMarkedText]) {
|
||||||
|
if (selector == @selector(cancelOperation:))
|
||||||
|
[self cancelMarked];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BOOL ownInput = self.currentInput == 0;
|
||||||
NSString *s = NSStringFromSelector(selector);
|
NSString *s = NSStringFromSelector(selector);
|
||||||
BOOL extend = [s containsString:@"AndModifySelection"];
|
BOOL extend = [s containsString:@"AndModifySelection"];
|
||||||
s = [s stringByReplacingOccurrencesOfString:@"AndModifySelection" withString:@""];
|
s = [s stringByReplacingOccurrencesOfString:@"AndModifySelection" withString:@""];
|
||||||
NSDictionary *commands = @{
|
NSDictionary *commands = @{
|
||||||
@"deleteBackward:" : @(UHDK_EDIT_DELETE_BACKWARD),
|
@"deleteBackward:" : @(UHDK_EDIT_DELETE_BACKWARD),
|
||||||
@"deleteForward:" : @(UHDK_EDIT_DELETE_FORWARD),
|
@"deleteForward:" : @(UHDK_EDIT_DELETE_FORWARD),
|
||||||
|
@"deleteWordBackward:" : @(UHDK_EDIT_DELETE_WORD_BACKWARD),
|
||||||
|
@"deleteWordForward:" : @(UHDK_EDIT_DELETE_WORD_FORWARD),
|
||||||
|
@"deleteToBeginningOfLine:" : @(UHDK_EDIT_DELETE_LINE_START),
|
||||||
|
@"deleteToEndOfLine:" : @(UHDK_EDIT_DELETE_LINE_END),
|
||||||
|
@"deleteToBeginningOfParagraph:" : @(UHDK_EDIT_DELETE_PARAGRAPH_START),
|
||||||
|
@"deleteToEndOfParagraph:" : @(UHDK_EDIT_DELETE_PARAGRAPH_END),
|
||||||
@"moveLeft:" : @(UHDK_EDIT_MOVE_LEFT),
|
@"moveLeft:" : @(UHDK_EDIT_MOVE_LEFT),
|
||||||
@"moveBackward:" : @(UHDK_EDIT_MOVE_LEFT),
|
@"moveBackward:" : @(UHDK_EDIT_MOVE_BACKWARD),
|
||||||
@"moveRight:" : @(UHDK_EDIT_MOVE_RIGHT),
|
@"moveRight:" : @(UHDK_EDIT_MOVE_RIGHT),
|
||||||
@"moveForward:" : @(UHDK_EDIT_MOVE_RIGHT),
|
@"moveForward:" : @(UHDK_EDIT_MOVE_FORWARD),
|
||||||
@"moveUp:" : @(UHDK_EDIT_MOVE_UP),
|
@"moveUp:" : @(UHDK_EDIT_MOVE_UP),
|
||||||
@"moveDown:" : @(UHDK_EDIT_MOVE_DOWN),
|
@"moveDown:" : @(UHDK_EDIT_MOVE_DOWN),
|
||||||
|
@"moveToLeftEndOfLine:" : @(UHDK_EDIT_LINE_LEFT_END),
|
||||||
|
@"moveToRightEndOfLine:" : @(UHDK_EDIT_LINE_RIGHT_END),
|
||||||
@"moveToBeginningOfLine:" : @(UHDK_EDIT_LINE_START),
|
@"moveToBeginningOfLine:" : @(UHDK_EDIT_LINE_START),
|
||||||
@"moveToEndOfLine:" : @(UHDK_EDIT_LINE_END),
|
@"moveToEndOfLine:" : @(UHDK_EDIT_LINE_END),
|
||||||
@"moveToBeginningOfDocument:" : @(UHDK_EDIT_DOCUMENT_START),
|
@"moveToBeginningOfDocument:" : @(UHDK_EDIT_DOCUMENT_START),
|
||||||
@"moveToEndOfDocument:" : @(UHDK_EDIT_DOCUMENT_END),
|
@"moveToEndOfDocument:" : @(UHDK_EDIT_DOCUMENT_END),
|
||||||
@"moveWordLeft:" : @(UHDK_EDIT_WORD_LEFT),
|
@"moveWordLeft:" : @(UHDK_EDIT_WORD_LEFT),
|
||||||
@"moveWordBackward:" : @(UHDK_EDIT_WORD_LEFT),
|
@"moveWordBackward:" : @(UHDK_EDIT_WORD_BACKWARD),
|
||||||
@"moveWordRight:" : @(UHDK_EDIT_WORD_RIGHT),
|
@"moveWordRight:" : @(UHDK_EDIT_WORD_RIGHT),
|
||||||
@"moveWordForward:" : @(UHDK_EDIT_WORD_RIGHT),
|
@"moveWordForward:" : @(UHDK_EDIT_WORD_FORWARD),
|
||||||
|
@"insertNewlineIgnoringFieldEditor:" : @(UHDK_EDIT_NEWLINE),
|
||||||
|
@"insertTabIgnoringFieldEditor:" : @(UHDK_EDIT_TAB),
|
||||||
@"insertNewline:" : @(UHDK_EDIT_NEWLINE),
|
@"insertNewline:" : @(UHDK_EDIT_NEWLINE),
|
||||||
@"insertTab:" : @(UHDK_EDIT_TAB),
|
@"insertTab:" : @(UHDK_EDIT_TAB),
|
||||||
|
@"insertBacktab:" : @(UHDK_EDIT_BACKTAB),
|
||||||
|
@"insertLineBreak:" : @(UHDK_EDIT_LINE_BREAK),
|
||||||
|
@"insertParagraphSeparator:" : @(UHDK_EDIT_NEWLINE),
|
||||||
|
@"cancelOperation:" : @(UHDK_EDIT_CANCEL),
|
||||||
|
@"moveParagraphBackward:" : @(UHDK_EDIT_PARAGRAPH_START),
|
||||||
|
@"moveParagraphForward:" : @(UHDK_EDIT_PARAGRAPH_END),
|
||||||
|
@"moveToBeginningOfParagraph:" : @(UHDK_EDIT_PARAGRAPH_START),
|
||||||
|
@"moveToEndOfParagraph:" : @(UHDK_EDIT_PARAGRAPH_END),
|
||||||
|
@"pageUp:" : @(UHDK_EDIT_PAGE_UP),
|
||||||
|
@"pageDown:" : @(UHDK_EDIT_PAGE_DOWN),
|
||||||
|
@"scrollPageUp:" : @(UHDK_EDIT_SCROLL_PAGE_UP),
|
||||||
|
@"scrollPageDown:" : @(UHDK_EDIT_SCROLL_PAGE_DOWN),
|
||||||
|
@"scrollToBeginningOfDocument:" : @(UHDK_EDIT_SCROLL_DOCUMENT_START),
|
||||||
|
@"scrollToEndOfDocument:" : @(UHDK_EDIT_SCROLL_DOCUMENT_END),
|
||||||
@"copy:" : @(UHDK_EDIT_COPY),
|
@"copy:" : @(UHDK_EDIT_COPY),
|
||||||
@"cut:" : @(UHDK_EDIT_CUT),
|
@"cut:" : @(UHDK_EDIT_CUT),
|
||||||
@"paste:" : @(UHDK_EDIT_PASTE),
|
@"paste:" : @(UHDK_EDIT_PASTE),
|
||||||
|
|
@ -536,49 +667,20 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
@"redo:" : @(UHDK_EDIT_REDO)
|
@"redo:" : @(UHDK_EDIT_REDO)
|
||||||
};
|
};
|
||||||
NSNumber *command = commands[s];
|
NSNumber *command = commands[s];
|
||||||
if (!command) {
|
if (!command)
|
||||||
if ([s isEqualToString:@"cancelOperation:"])
|
|
||||||
[self cancelMarked];
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
[self editID];
|
[self editID];
|
||||||
z_event e = {.type = UHDK_EVENT_EDIT_COMMAND};
|
z_event e = {.type = UHDK_EVENT_EDIT_COMMAND};
|
||||||
e.data.edit.command = command.unsignedIntValue;
|
e.data.edit.command = command.unsignedIntValue;
|
||||||
e.data.edit.extend_selection = extend;
|
e.data.edit.extend_selection =
|
||||||
|
extend && ([s hasPrefix:@"move"] || [s isEqualToString:@"pageUp:"] ||
|
||||||
|
[s isEqualToString:@"pageDown:"]);
|
||||||
|
// The application owns layout and edit policy. Never predict its selection
|
||||||
|
// or document changes using a second, potentially different text engine.
|
||||||
|
self.awaitingSnapshot = YES;
|
||||||
[self emit:&e];
|
[self emit:&e];
|
||||||
// Keep common edit operations coherent for subsequent input before an ack.
|
if (ownInput)
|
||||||
NSRange r = self.selection;
|
self.currentInput = 0;
|
||||||
if (!extend) {
|
|
||||||
if (command.unsignedIntValue == UHDK_EDIT_SELECT_ALL)
|
|
||||||
self.selection = NSMakeRange(0, self.textBuffer.length);
|
|
||||||
else if (command.unsignedIntValue == UHDK_EDIT_DELETE_BACKWARD ||
|
|
||||||
command.unsignedIntValue == UHDK_EDIT_DELETE_FORWARD) {
|
|
||||||
if (!r.length && command.unsignedIntValue == UHDK_EDIT_DELETE_BACKWARD && r.location)
|
|
||||||
r = [self.textBuffer rangeOfComposedCharacterSequenceAtIndex:r.location - 1];
|
|
||||||
else if (!r.length && r.location < self.textBuffer.length &&
|
|
||||||
command.unsignedIntValue == UHDK_EDIT_DELETE_FORWARD)
|
|
||||||
r = [self.textBuffer rangeOfComposedCharacterSequenceAtIndex:r.location];
|
|
||||||
[self.textBuffer deleteCharactersInRange:r];
|
|
||||||
self.selection = NSMakeRange(r.location, 0);
|
|
||||||
} else if (command.unsignedIntValue == UHDK_EDIT_NEWLINE ||
|
|
||||||
command.unsignedIntValue == UHDK_EDIT_TAB) {
|
|
||||||
[self.textBuffer
|
|
||||||
replaceCharactersInRange:r
|
|
||||||
withString:command.unsignedIntValue == UHDK_EDIT_NEWLINE ? @"\n"
|
|
||||||
: @"\t"];
|
|
||||||
self.selection = NSMakeRange(r.location + 1, 0);
|
|
||||||
} else if (command.unsignedIntValue == UHDK_EDIT_MOVE_LEFT) {
|
|
||||||
NSUInteger end = r.location;
|
|
||||||
if (!r.length && end)
|
|
||||||
end = [self.textBuffer rangeOfComposedCharacterSequenceAtIndex:end - 1].location;
|
|
||||||
self.selection = NSMakeRange(end, 0);
|
|
||||||
} else if (command.unsignedIntValue == UHDK_EDIT_MOVE_RIGHT) {
|
|
||||||
NSUInteger end = NSMaxRange(r);
|
|
||||||
if (!r.length && end < self.textBuffer.length)
|
|
||||||
end = NSMaxRange([self.textBuffer rangeOfComposedCharacterSequenceAtIndex:end]);
|
|
||||||
self.selection = NSMakeRange(end, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
|
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
|
||||||
return ([sender.draggingPasteboard
|
return ([sender.draggingPasteboard
|
||||||
|
|
@ -618,6 +720,30 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
@implementation ZWindow
|
@implementation ZWindow
|
||||||
|
- (void)applyPendingState {
|
||||||
|
if (!self.hasPendingState || self.fullscreenTransition)
|
||||||
|
return;
|
||||||
|
uint32_t state = self.pendingState;
|
||||||
|
self.hasPendingState = NO;
|
||||||
|
BOOL full = (self.window.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
||||||
|
if ((state == UHDK_WINDOW_FULLSCREEN) != full) {
|
||||||
|
if (self.window.isMiniaturized)
|
||||||
|
[self.window deminiaturize:nil];
|
||||||
|
self.hasPendingState = full; // Apply minimize/zoom after leaving fullscreen.
|
||||||
|
self.fullscreenTransition = YES;
|
||||||
|
[self.window toggleFullScreen:nil];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state == UHDK_WINDOW_MINIMIZED)
|
||||||
|
[self.window miniaturize:nil];
|
||||||
|
else {
|
||||||
|
if (self.window.isMiniaturized)
|
||||||
|
[self.window deminiaturize:nil];
|
||||||
|
if (!full && ((state == UHDK_WINDOW_MAXIMIZED) != self.window.isZoomed))
|
||||||
|
[self.window zoom:nil];
|
||||||
|
}
|
||||||
|
[self metrics];
|
||||||
|
}
|
||||||
- (void)metrics {
|
- (void)metrics {
|
||||||
if (!self.window || !self.view)
|
if (!self.window || !self.view)
|
||||||
return;
|
return;
|
||||||
|
|
@ -628,15 +754,21 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
CGLUnlockContext(gl);
|
CGLUnlockContext(gl);
|
||||||
}
|
}
|
||||||
NSRect bounds = self.view.bounds, pixels = [self.view convertRectToBacking:bounds];
|
NSRect bounds = self.view.bounds, pixels = [self.view convertRectToBacking:bounds];
|
||||||
uhdk_window_metrics m = {.struct_size = sizeof(m),
|
uhdk_window_metrics m = {
|
||||||
|
.struct_size = sizeof(m),
|
||||||
.logical_size = {bounds.size.width, bounds.size.height},
|
.logical_size = {bounds.size.width, bounds.size.height},
|
||||||
.framebuffer_width = (uint32_t)MAX(0, pixels.size.width),
|
.framebuffer_width = (uint32_t)MAX(0, pixels.size.width),
|
||||||
.framebuffer_height = (uint32_t)MAX(0, pixels.size.height),
|
.framebuffer_height = (uint32_t)MAX(0, pixels.size.height),
|
||||||
.scale = self.window.backingScaleFactor,
|
.scale = self.window.backingScaleFactor,
|
||||||
.focused = self.window.isKeyWindow,
|
.focused = self.window.isKeyWindow,
|
||||||
.visible = self.window.isVisible};
|
.visible = self.window.isVisible,
|
||||||
m.state = self.window.isMiniaturized ? UHDK_WINDOW_MINIMIZED
|
.observations = UHDK_WINDOW_OBSERVE_MINIMIZED | UHDK_WINDOW_OBSERVE_RESIZING |
|
||||||
: (self.window.styleMask & NSWindowStyleMaskFullScreen) ? UHDK_WINDOW_FULLSCREEN
|
UHDK_WINDOW_OBSERVE_OCCLUDED,
|
||||||
|
.minimized = self.window.isMiniaturized,
|
||||||
|
.resizing = self.resizing,
|
||||||
|
.occluded = !(self.window.occlusionState & NSWindowOcclusionStateVisible)
|
||||||
|
};
|
||||||
|
m.state = (self.window.styleMask & NSWindowStyleMaskFullScreen) ? UHDK_WINDOW_FULLSCREEN
|
||||||
: self.window.isZoomed ? UHDK_WINDOW_MAXIMIZED
|
: self.window.isZoomed ? UHDK_WINDOW_MAXIMIZED
|
||||||
: UHDK_WINDOW_NORMAL;
|
: UHDK_WINDOW_NORMAL;
|
||||||
z_metrics(self.handle, &m);
|
z_metrics(self.handle, &m);
|
||||||
|
|
@ -653,6 +785,10 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
(void)n;
|
(void)n;
|
||||||
[self metrics];
|
[self metrics];
|
||||||
}
|
}
|
||||||
|
- (void)windowDidChangeOcclusionState:(NSNotification *)n {
|
||||||
|
(void)n;
|
||||||
|
[self metrics];
|
||||||
|
}
|
||||||
- (void)windowDidChangeBackingProperties:(NSNotification *)n {
|
- (void)windowDidChangeBackingProperties:(NSNotification *)n {
|
||||||
(void)n;
|
(void)n;
|
||||||
[self metrics];
|
[self metrics];
|
||||||
|
|
@ -665,6 +801,8 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
- (void)windowDidResignKey:(NSNotification *)n {
|
- (void)windowDidResignKey:(NSNotification *)n {
|
||||||
(void)n;
|
(void)n;
|
||||||
[self.view.pressedModifiers removeAllIndexes];
|
[self.view.pressedModifiers removeAllIndexes];
|
||||||
|
[self.view.deferredInput removeAllObjects];
|
||||||
|
self.view.awaitingSnapshot = NO;
|
||||||
[self.view cancelMarked];
|
[self.view cancelMarked];
|
||||||
if (self.view.capture) {
|
if (self.view.capture) {
|
||||||
self.view.capture = NO;
|
self.view.capture = NO;
|
||||||
|
|
@ -683,13 +821,34 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
(void)n;
|
(void)n;
|
||||||
[self metrics];
|
[self metrics];
|
||||||
}
|
}
|
||||||
|
- (void)windowWillEnterFullScreen:(NSNotification *)n {
|
||||||
|
(void)n;
|
||||||
|
self.fullscreenTransition = YES;
|
||||||
|
}
|
||||||
|
- (void)windowWillExitFullScreen:(NSNotification *)n {
|
||||||
|
(void)n;
|
||||||
|
self.fullscreenTransition = YES;
|
||||||
|
}
|
||||||
|
- (void)windowDidFailToEnterFullScreen:(NSWindow *)window {
|
||||||
|
(void)window;
|
||||||
|
self.fullscreenTransition = NO;
|
||||||
|
self.hasPendingState = NO;
|
||||||
|
[self metrics];
|
||||||
|
}
|
||||||
|
- (void)windowDidFailToExitFullScreen:(NSWindow *)window {
|
||||||
|
[self windowDidFailToEnterFullScreen:window];
|
||||||
|
}
|
||||||
- (void)windowDidEnterFullScreen:(NSNotification *)n {
|
- (void)windowDidEnterFullScreen:(NSNotification *)n {
|
||||||
(void)n;
|
(void)n;
|
||||||
|
self.fullscreenTransition = NO;
|
||||||
[self metrics];
|
[self metrics];
|
||||||
|
[self applyPendingState];
|
||||||
}
|
}
|
||||||
- (void)windowDidExitFullScreen:(NSNotification *)n {
|
- (void)windowDidExitFullScreen:(NSNotification *)n {
|
||||||
(void)n;
|
(void)n;
|
||||||
|
self.fullscreenTransition = NO;
|
||||||
[self metrics];
|
[self metrics];
|
||||||
|
[self applyPendingState];
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
@interface ZMenuAction : NSObject
|
@interface ZMenuAction : NSObject
|
||||||
|
|
@ -715,12 +874,11 @@ static uhdk_key logical(NSEvent *e) {
|
||||||
e.type = UHDK_EVENT_QUIT_REQUESTED;
|
e.type = UHDK_EVENT_QUIT_REQUESTED;
|
||||||
e.window = NULL;
|
e.window = NULL;
|
||||||
} else if (self.role >= UHDK_MENU_ROLE_COPY && self.role <= UHDK_MENU_ROLE_REDO) {
|
} else if (self.role >= UHDK_MENU_ROLE_COPY && self.role <= UHDK_MENU_ROLE_REDO) {
|
||||||
static const uint32_t edit[] = {UHDK_EDIT_COPY, UHDK_EDIT_CUT, UHDK_EDIT_PASTE,
|
static const char *selectors[] = {
|
||||||
UHDK_EDIT_SELECT_ALL, UHDK_EDIT_UNDO, UHDK_EDIT_REDO};
|
"copy:", "cut:", "paste:", "selectAll:", "undo:", "redo:"
|
||||||
e.type = UHDK_EVENT_EDIT_COMMAND;
|
};
|
||||||
e.data.edit.command = edit[self.role - UHDK_MENU_ROLE_COPY];
|
[w.view doCommandBySelector:sel_registerName(selectors[self.role - UHDK_MENU_ROLE_COPY])];
|
||||||
e.input_id = ++w.view.nextInput;
|
return;
|
||||||
w.view.lastEdit = e.input_id;
|
|
||||||
}
|
}
|
||||||
z_emit(host.app, &e);
|
z_emit(host.app, &e);
|
||||||
}
|
}
|
||||||
|
|
@ -1316,21 +1474,11 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
|
||||||
[w.window setContentSize:NSMakeSize(cmd->size.width, cmd->size.height)];
|
[w.window setContentSize:NSMakeSize(cmd->size.width, cmd->size.height)];
|
||||||
[w metrics];
|
[w metrics];
|
||||||
break;
|
break;
|
||||||
case Z_STATE: {
|
case Z_STATE:
|
||||||
BOOL full = (w.window.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
w.pendingState = (uint32_t)cmd->number;
|
||||||
if ((cmd->number == UHDK_WINDOW_FULLSCREEN) != full)
|
w.hasPendingState = YES;
|
||||||
[w.window toggleFullScreen:nil];
|
[w applyPendingState];
|
||||||
else if (cmd->number == UHDK_WINDOW_MINIMIZED)
|
|
||||||
[w.window miniaturize:nil];
|
|
||||||
else {
|
|
||||||
if (w.window.isMiniaturized)
|
|
||||||
[w.window deminiaturize:nil];
|
|
||||||
if ((cmd->number == UHDK_WINDOW_MAXIMIZED) != w.window.isZoomed)
|
|
||||||
[w.window zoom:nil];
|
|
||||||
}
|
|
||||||
[w metrics];
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case Z_VISIBLE:
|
case Z_VISIBLE:
|
||||||
if (cmd->number)
|
if (cmd->number)
|
||||||
[w.window orderFront:nil];
|
[w.window orderFront:nil];
|
||||||
|
|
@ -1436,8 +1584,11 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
|
||||||
break;
|
break;
|
||||||
case Z_TEXT_ENABLE:
|
case Z_TEXT_ENABLE:
|
||||||
w.view.textEnabled = cmd->number != 0;
|
w.view.textEnabled = cmd->number != 0;
|
||||||
if (!cmd->number)
|
if (!cmd->number) {
|
||||||
|
[w.view.deferredInput removeAllObjects];
|
||||||
|
w.view.awaitingSnapshot = NO;
|
||||||
[w.view cancelMarked];
|
[w.view cancelMarked];
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Z_TEXT_STATE:
|
case Z_TEXT_STATE:
|
||||||
[w.view applyState:&cmd->text_state];
|
[w.view applyState:&cmd->text_state];
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#define UHDK_ENABLE_NATIVE
|
#define UHDK_ENABLE_NATIVE
|
||||||
#include "uhdk.h"
|
#include "uhdk.h"
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include "edit_keys.h"
|
||||||
/* Private producer storage. Converted to public pointer payloads at poll. */
|
/* Private producer storage. Converted to public pointer payloads at poll. */
|
||||||
typedef struct z_request_result {
|
typedef struct z_request_result {
|
||||||
uhdk_request_id id;
|
uhdk_request_id id;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ struct Window {
|
||||||
uint32_t flags;
|
uint32_t flags;
|
||||||
uhdk_size min_size, max_size;
|
uhdk_size min_size, max_size;
|
||||||
float scale;
|
float scale;
|
||||||
BOOL text_enabled, tracking, fullscreen, composing, suppress_char;
|
BOOL text_enabled, tracking, fullscreen, composing, suppress_char, resizing;
|
||||||
WCHAR high_surrogate;
|
WCHAR high_surrogate;
|
||||||
uint64_t input_id, last_edit;
|
uint64_t input_id, last_edit;
|
||||||
uhdk_text_state text_state;
|
uhdk_text_state text_state;
|
||||||
|
|
@ -180,9 +180,15 @@ static void metrics(Window *w) {
|
||||||
m.framebuffer_width = (uint32_t)(r.right - r.left);
|
m.framebuffer_width = (uint32_t)(r.right - r.left);
|
||||||
m.framebuffer_height = (uint32_t)(r.bottom - r.top);
|
m.framebuffer_height = (uint32_t)(r.bottom - r.top);
|
||||||
m.logical_size = (uhdk_size){m.framebuffer_width / w->scale, m.framebuffer_height / w->scale};
|
m.logical_size = (uhdk_size){m.framebuffer_width / w->scale, m.framebuffer_height / w->scale};
|
||||||
|
WINDOWPLACEMENT placement = {0};
|
||||||
|
placement.length = sizeof(placement);
|
||||||
|
GetWindowPlacement(w->hwnd, &placement);
|
||||||
|
m.observations = UHDK_WINDOW_OBSERVE_MINIMIZED | UHDK_WINDOW_OBSERVE_RESIZING;
|
||||||
|
m.minimized = IsIconic(w->hwnd);
|
||||||
|
m.resizing = w->resizing;
|
||||||
m.state = w->fullscreen ? UHDK_WINDOW_FULLSCREEN
|
m.state = w->fullscreen ? UHDK_WINDOW_FULLSCREEN
|
||||||
: IsIconic(w->hwnd) ? UHDK_WINDOW_MINIMIZED
|
: (IsZoomed(w->hwnd) || (m.minimized && (placement.flags & WPF_RESTORETOMAXIMIZED)))
|
||||||
: IsZoomed(w->hwnd) ? UHDK_WINDOW_MAXIMIZED
|
? UHDK_WINDOW_MAXIMIZED
|
||||||
: UHDK_WINDOW_NORMAL;
|
: UHDK_WINDOW_NORMAL;
|
||||||
m.focused = GetFocus() == w->hwnd;
|
m.focused = GetFocus() == w->hwnd;
|
||||||
m.visible = IsWindowVisible(w->hwnd);
|
m.visible = IsWindowVisible(w->hwnd);
|
||||||
|
|
@ -228,13 +234,15 @@ static void textEvent(Window *w, uint32_t type, const WCHAR *text, int length, i
|
||||||
profileFree(s);
|
profileFree(s);
|
||||||
}
|
}
|
||||||
static void editEvent(Window *w, uint32_t command) {
|
static void editEvent(Window *w, uint32_t command) {
|
||||||
|
if (w->composing)
|
||||||
|
return;
|
||||||
z_event e = {0};
|
z_event e = {0};
|
||||||
e.type = UHDK_EVENT_EDIT_COMMAND;
|
e.type = UHDK_EVENT_EDIT_COMMAND;
|
||||||
e.window = w->handle;
|
e.window = w->handle;
|
||||||
e.input_id = w->input_id = ++host.input_id;
|
e.input_id = w->input_id = ++host.input_id;
|
||||||
w->last_edit = e.input_id;
|
w->last_edit = e.input_id;
|
||||||
e.data.edit.command = command;
|
e.data.edit.command = command;
|
||||||
e.data.edit.extend_selection = !!(modifiers() & UHDK_MOD_SHIFT);
|
e.data.edit.extend_selection = (uhdk_bool)z_edit_extends_selection(command, modifiers());
|
||||||
z_emit(host.app, &e);
|
z_emit(host.app, &e);
|
||||||
}
|
}
|
||||||
static void menuEvent(Window *w, const uhdk_menu_item *item) {
|
static void menuEvent(Window *w, const uhdk_menu_item *item) {
|
||||||
|
|
@ -366,29 +374,6 @@ static uint32_t physicalKey(LPARAM param) {
|
||||||
}
|
}
|
||||||
return keys[scan];
|
return keys[scan];
|
||||||
}
|
}
|
||||||
static uint32_t editKey(WPARAM key, uint32_t mods) {
|
|
||||||
BOOL ctrl = !!(mods & UHDK_MOD_CONTROL);
|
|
||||||
switch (key) {
|
|
||||||
case VK_BACK:
|
|
||||||
return UHDK_EDIT_DELETE_BACKWARD;
|
|
||||||
case VK_DELETE:
|
|
||||||
return UHDK_EDIT_DELETE_FORWARD;
|
|
||||||
case VK_LEFT:
|
|
||||||
return ctrl ? UHDK_EDIT_WORD_LEFT : UHDK_EDIT_MOVE_LEFT;
|
|
||||||
case VK_RIGHT:
|
|
||||||
return ctrl ? UHDK_EDIT_WORD_RIGHT : UHDK_EDIT_MOVE_RIGHT;
|
|
||||||
case VK_UP:
|
|
||||||
return UHDK_EDIT_MOVE_UP;
|
|
||||||
case VK_DOWN:
|
|
||||||
return UHDK_EDIT_MOVE_DOWN;
|
|
||||||
case VK_HOME:
|
|
||||||
return ctrl ? UHDK_EDIT_DOCUMENT_START : UHDK_EDIT_LINE_START;
|
|
||||||
case VK_END:
|
|
||||||
return ctrl ? UHDK_EDIT_DOCUMENT_END : UHDK_EDIT_LINE_END;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static void imePosition(Window *w) {
|
static void imePosition(Window *w) {
|
||||||
HIMC ime = ImmGetContext(w->hwnd);
|
HIMC ime = ImmGetContext(w->hwnd);
|
||||||
if (!ime)
|
if (!ime)
|
||||||
|
|
@ -497,6 +482,14 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||||
}
|
}
|
||||||
case WM_ERASEBKGND:
|
case WM_ERASEBKGND:
|
||||||
return 1;
|
return 1;
|
||||||
|
case WM_ENTERSIZEMOVE:
|
||||||
|
w->resizing = TRUE;
|
||||||
|
metrics(w);
|
||||||
|
break;
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
w->resizing = FALSE;
|
||||||
|
metrics(w);
|
||||||
|
break;
|
||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
case WM_SHOWWINDOW:
|
case WM_SHOWWINDOW:
|
||||||
metrics(w);
|
metrics(w);
|
||||||
|
|
@ -657,7 +650,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||||
w->suppress_char = FALSE;
|
w->suppress_char = FALSE;
|
||||||
const uhdk_menu_item *item =
|
const uhdk_menu_item *item =
|
||||||
shortcut(w->menu_source ? w->menu_source : host.menu, key, mods & 15);
|
shortcut(w->menu_source ? w->menu_source : host.menu, key, mods & 15);
|
||||||
if (item) {
|
if (item && !w->composing) {
|
||||||
w->suppress_char = TRUE;
|
w->suppress_char = TRUE;
|
||||||
menuEvent(w, item);
|
menuEvent(w, item);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -674,9 +667,12 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||||
e.data.key.repeat = down && !!(lp & (1u << 30));
|
e.data.key.repeat = down && !!(lp & (1u << 30));
|
||||||
z_emit(host.app, &e);
|
z_emit(host.app, &e);
|
||||||
if (down && w->text_enabled && !w->composing) {
|
if (down && w->text_enabled && !w->composing) {
|
||||||
uint32_t command = editKey(wp, mods);
|
uint32_t command = z_edit_key(key, mods);
|
||||||
if (command)
|
if (command) {
|
||||||
|
w->suppress_char = TRUE;
|
||||||
editEvent(w, command);
|
editEvent(w, command);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -686,14 +682,8 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||||
return TRUE;
|
return TRUE;
|
||||||
if (!w->text_enabled || w->composing || w->suppress_char)
|
if (!w->text_enabled || w->composing || w->suppress_char)
|
||||||
return 0;
|
return 0;
|
||||||
if (wp == '\r') {
|
// Enter/Tab are semantic keydown commands. Ignore their translated
|
||||||
editEvent(w, UHDK_EDIT_NEWLINE);
|
// WM_CHAR messages even if another queued key changed suppress_char.
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (wp == '\t') {
|
|
||||||
editEvent(w, UHDK_EDIT_TAB);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (wp < 32 || wp == 127)
|
if (wp < 32 || wp == 127)
|
||||||
return 0;
|
return 0;
|
||||||
WCHAR chars[2];
|
WCHAR chars[2];
|
||||||
|
|
@ -1703,8 +1693,11 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Z_STATE: {
|
case Z_STATE: {
|
||||||
BOOL full = cmd->number == UHDK_WINDOW_FULLSCREEN;
|
BOOL minimize = cmd->number == UHDK_WINDOW_MINIMIZED;
|
||||||
|
BOOL full = cmd->number == UHDK_WINDOW_FULLSCREEN || (minimize && w->fullscreen);
|
||||||
if (full && !w->fullscreen) {
|
if (full && !w->fullscreen) {
|
||||||
|
if (IsIconic(w->hwnd))
|
||||||
|
ShowWindow(w->hwnd, SW_RESTORE);
|
||||||
w->placement.length = sizeof(w->placement);
|
w->placement.length = sizeof(w->placement);
|
||||||
GetWindowPlacement(w->hwnd, &w->placement);
|
GetWindowPlacement(w->hwnd, &w->placement);
|
||||||
w->saved_style = GetWindowLongPtrW(w->hwnd, GWL_STYLE);
|
w->saved_style = GetWindowLongPtrW(w->hwnd, GWL_STYLE);
|
||||||
|
|
@ -1724,10 +1717,12 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
|
||||||
SetWindowPos(w->hwnd, NULL, 0, 0, 0, 0,
|
SetWindowPos(w->hwnd, NULL, 0, 0, 0, 0,
|
||||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||||
}
|
}
|
||||||
if (!full)
|
if (minimize)
|
||||||
ShowWindow(w->hwnd, cmd->number == UHDK_WINDOW_MINIMIZED ? SW_MINIMIZE
|
ShowWindow(w->hwnd, SW_MINIMIZE);
|
||||||
: cmd->number == UHDK_WINDOW_MAXIMIZED ? SW_MAXIMIZE
|
else if (!full)
|
||||||
: SW_RESTORE);
|
ShowWindow(w->hwnd, cmd->number == UHDK_WINDOW_MAXIMIZED ? SW_MAXIMIZE : SW_RESTORE);
|
||||||
|
else if (IsIconic(w->hwnd))
|
||||||
|
ShowWindow(w->hwnd, SW_RESTORE);
|
||||||
metrics(w);
|
metrics(w);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue