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
|
||||
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
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -324,8 +324,25 @@ typedef struct uhdk_window_desc {
|
|||
uhdk_window *parent; /* Optional; must outlive child. */
|
||||
} uhdk_window_desc;
|
||||
|
||||
/* On Wayland, visible is application show/hide intent, not compositor visibility.
|
||||
* State is the last observable state; Wayland does not report minimization.
|
||||
/* Observation bits say which independent window facts are known. A clear bit
|
||||
* 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. */
|
||||
typedef struct uhdk_window_metrics {
|
||||
uint32_t struct_size;
|
||||
|
|
@ -336,6 +353,11 @@ typedef struct uhdk_window_metrics {
|
|||
float scale;
|
||||
uhdk_bool focused;
|
||||
uhdk_bool visible;
|
||||
uhdk_window_observation_flags observations;
|
||||
uhdk_bool minimized;
|
||||
uhdk_bool resizing;
|
||||
uhdk_bool occluded;
|
||||
uhdk_bool suspended;
|
||||
} uhdk_window_metrics;
|
||||
|
||||
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.
|
||||
* acknowledged_input_id is the last applied text/edit input ID. Snapshots behind
|
||||
* 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
|
||||
* updates temporary composition, never the permanent document by itself.
|
||||
*/
|
||||
|
|
@ -870,6 +897,19 @@ enum {
|
|||
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;
|
||||
enum {
|
||||
UHDK_EDIT_COPY = 1,
|
||||
|
|
@ -891,7 +931,30 @@ enum {
|
|||
UHDK_EDIT_WORD_LEFT,
|
||||
UHDK_EDIT_WORD_RIGHT,
|
||||
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.
|
||||
|
|
@ -939,7 +1002,7 @@ typedef struct uhdk_text_event {
|
|||
|
||||
typedef struct uhdk_edit_event {
|
||||
uhdk_edit_command command;
|
||||
uhdk_bool extend_selection;
|
||||
uhdk_bool extend_selection; /* Only meaningful for caret movement. */
|
||||
} uhdk_edit_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.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);
|
||||
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;
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,31 +48,9 @@ pub fn logical(sym: u32) u32 {
|
|||
};
|
||||
}
|
||||
pub fn edit(key: u32, mods: u32) u32 {
|
||||
const ctrl = mods & n.UHDK_MOD_CONTROL != 0;
|
||||
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,
|
||||
};
|
||||
return n.z_edit_key(key, mods);
|
||||
}
|
||||
|
||||
test "physical positions and editor shortcuts" {
|
||||
try std.testing.expectEqual(@as(u32, 4), physical(30));
|
||||
try std.testing.expectEqual(@as(u32, 29), physical(44));
|
||||
|
|
|
|||
|
|
@ -4,17 +4,14 @@ const n = @import("native");
|
|||
const native = @import("native.zig");
|
||||
const runtime = @import("../runtime.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 {
|
||||
var event = std.mem.zeroes(n.z_event);
|
||||
if (item.role == n.UHDK_MENU_ROLE_QUIT) {
|
||||
event.type = n.UHDK_EVENT_QUIT_REQUESTED;
|
||||
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) {
|
||||
event.type = n.UHDK_EVENT_EDIT_COMMAND;
|
||||
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);
|
||||
Text.editCommand(window, @as(u32, n.UHDK_EDIT_COPY) + item.role - n.UHDK_MENU_ROLE_COPY, 0, input_id);
|
||||
} else {
|
||||
event.type = n.UHDK_EVENT_MENU_COMMAND;
|
||||
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.
|
||||
- Size/state commands acknowledge submission. Confirmed geometry and focus arrive
|
||||
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.
|
||||
- Fractional scale uses fractional-scale-v1 plus viewporter; otherwise preferred
|
||||
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
|
||||
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
|
||||
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.
|
||||
Capture acknowledges Wayland's implicit button grab during a selection drag;
|
||||
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
|
||||
# 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/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/menu_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 t = &w.text;
|
||||
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
|
||||
// batch may still refer to. Keep the newest application state for later.
|
||||
if (t.sent_count >= 32) {
|
||||
|
|
@ -277,20 +281,41 @@ pub fn deinit(w: *native.Window) void {
|
|||
forget(w);
|
||||
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 {
|
||||
const s = Input.state();
|
||||
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);
|
||||
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);
|
||||
endAt(w, input_id);
|
||||
return;
|
||||
}
|
||||
if (s.compose != null) c.z_xkb_compose_state_reset.?(s.compose);
|
||||
endAt(w, input_id);
|
||||
var e = std.mem.zeroes(n.z_event);
|
||||
e.type = n.UHDK_EVENT_EDIT_COMMAND;
|
||||
e.input_id = input_id;
|
||||
e.data.edit = .{ .command = edit, .extend_selection = @intFromBool(modifiers & n.UHDK_MOD_SHIFT != 0) };
|
||||
t.last_edit = input_id;
|
||||
Input.send(w, &e);
|
||||
editCommand(w, edit, modifiers, input_id);
|
||||
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 Input = @import("Input.zig");
|
||||
const Desktop = @import("Desktop.zig");
|
||||
const WindowState = @import("WindowState.zig");
|
||||
const Portal = @import("Portal.zig");
|
||||
const Watch = @import("Watch.zig");
|
||||
const Data = @import("Data.zig");
|
||||
|
|
@ -44,6 +45,8 @@ pub const Window = struct {
|
|||
scale: u32 = 120,
|
||||
focused: bool = false,
|
||||
state: u32 = n.UHDK_WINDOW_NORMAL,
|
||||
observation: WindowState.Observation = .{},
|
||||
wm_caps: WindowState.Capabilities = .{},
|
||||
flags: u32,
|
||||
cursor: u32 = n.UHDK_CURSOR_ARROW,
|
||||
text: Text.State = .{},
|
||||
|
|
@ -358,20 +361,10 @@ fn topConfigure(data: ?*anyopaque, _: ?*c.struct_xdg_toplevel, width: i32, heigh
|
|||
const w = window(data);
|
||||
if (width > 0) w.width = width;
|
||||
if (height > 0) w.height = height;
|
||||
w.focused = false;
|
||||
w.state = n.UHDK_WINDOW_NORMAL;
|
||||
if (states.*.size == 0) return;
|
||||
const values: [*]const u32 = @ptrCast(@alignCast(states.*.data));
|
||||
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 => {},
|
||||
}
|
||||
}
|
||||
const values: []const u32 = if (states.*.size == 0) &.{} else @as([*]const u32, @ptrCast(@alignCast(states.*.data)))[0 .. states.*.size / @sizeOf(u32)];
|
||||
w.observation = WindowState.observe(c.xdg_toplevel_get_version(w.top), values);
|
||||
w.focused = w.observation.focused;
|
||||
w.state = w.observation.geometry;
|
||||
}
|
||||
fn emit(w: *Window, kind: u32) void {
|
||||
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);
|
||||
}
|
||||
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 };
|
||||
fn decorationConfigure(data: ?*anyopaque, _: ?*c.struct_zxdg_toplevel_decoration_v1, mode: u32) callconv(.c) void {
|
||||
const w = window(data);
|
||||
|
|
@ -454,7 +450,7 @@ fn apply(w: *Window) void {
|
|||
w.configured = false;
|
||||
}
|
||||
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;
|
||||
w.dirty = false;
|
||||
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;
|
||||
},
|
||||
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 => {
|
||||
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);
|
||||
},
|
||||
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 => {
|
||||
c.xdg_toplevel_unset_fullscreen(w.top);
|
||||
c.xdg_toplevel_unset_maximized(w.top);
|
||||
if (w.wm_caps.fullscreen) c.xdg_toplevel_unset_fullscreen(w.top);
|
||||
if (w.wm_caps.maximize) c.xdg_toplevel_unset_maximized(w.top);
|
||||
},
|
||||
},
|
||||
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);
|
||||
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) {
|
||||
uhdk_window *w = window(app);
|
||||
uint64_t id;
|
||||
|
|
@ -379,6 +502,14 @@ static void run(uhdk_app *app, void *mode) {
|
|||
menus(app);
|
||||
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)
|
||||
input(app);
|
||||
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) uhdk_menu *menu;
|
||||
@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;
|
||||
@end
|
||||
@interface ZView : NSView <NSTextInputClient>
|
||||
|
|
@ -75,6 +80,8 @@ static uhdk_modifier_flags mods(NSEventModifierFlags f) {
|
|||
@property(nonatomic) uint64_t currentInput;
|
||||
@property(nonatomic) uint64_t lastEdit;
|
||||
@property(nonatomic) uhdk_rect caret;
|
||||
@property(nonatomic) BOOL awaitingSnapshot;
|
||||
@property(nonatomic, strong) NSMutableArray<void (^)(void)> *deferredInput;
|
||||
- (void)emit:(z_event *)event;
|
||||
- (void)cancelMarked;
|
||||
- (void)applyState:(const uhdk_text_state *)state;
|
||||
|
|
@ -183,6 +190,7 @@ static uhdk_key logical(NSEvent *e) {
|
|||
_marked = NSMakeRange(NSNotFound, 0);
|
||||
_cursor = NSCursor.arrowCursor;
|
||||
_pressedModifiers = [NSMutableIndexSet indexSet];
|
||||
_deferredInput = [NSMutableArray array];
|
||||
self.wantsBestResolutionOpenGLSurface = YES;
|
||||
}
|
||||
return self;
|
||||
|
|
@ -211,6 +219,16 @@ static uhdk_key logical(NSEvent *e) {
|
|||
[super setFrameSize:size];
|
||||
[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 {
|
||||
[super viewDidChangeBackingProperties];
|
||||
[self.owner metrics];
|
||||
|
|
@ -235,6 +253,13 @@ static uhdk_key logical(NSEvent *e) {
|
|||
[self.cursor set];
|
||||
}
|
||||
- (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;
|
||||
z_event e = {.type = UHDK_EVENT_KEY};
|
||||
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.repeat = event.isARepeat;
|
||||
[self emit:&e];
|
||||
if (pressed && self.textEnabled && !(event.modifierFlags & NSEventModifierFlagCommand))
|
||||
if (pressed && self.textEnabled)
|
||||
[self interpretKeyEvents:@[ event ]];
|
||||
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 {
|
||||
[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);
|
||||
self.selection = NSMakeRange(start, end - start);
|
||||
[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
|
||||
type:(uint32_t)type
|
||||
|
|
@ -395,6 +471,14 @@ static uhdk_key logical(NSEvent *e) {
|
|||
[self emit:&e];
|
||||
}
|
||||
- (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;
|
||||
if (ownInput)
|
||||
self.currentInput = ++self.nextInput;
|
||||
|
|
@ -425,6 +509,14 @@ static uhdk_key logical(NSEvent *e) {
|
|||
self.currentInput = 0;
|
||||
}
|
||||
- (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;
|
||||
if (ownInput)
|
||||
self.currentInput = ++self.nextInput;
|
||||
|
|
@ -477,14 +569,14 @@ static uhdk_key logical(NSEvent *e) {
|
|||
return self.marked;
|
||||
}
|
||||
- (NSRange)selectedRange {
|
||||
return self.selection;
|
||||
return self.awaitingSnapshot ? NSMakeRange(NSNotFound, 0) : self.selection;
|
||||
}
|
||||
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
|
||||
return @[];
|
||||
}
|
||||
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)r
|
||||
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)
|
||||
*actual = NSMakeRange(NSNotFound, 0);
|
||||
return nil;
|
||||
|
|
@ -501,33 +593,72 @@ static uhdk_key logical(NSEvent *e) {
|
|||
- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actual {
|
||||
(void)range;
|
||||
if (actual)
|
||||
*actual = self.selection;
|
||||
*actual = [self selectedRange];
|
||||
NSRect rect = NSMakeRect(self.caret.x, self.caret.y, self.caret.width, self.caret.height);
|
||||
return [self.window convertRectToScreen:[self convertRect:rect toView:nil]];
|
||||
}
|
||||
- (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);
|
||||
BOOL extend = [s containsString:@"AndModifySelection"];
|
||||
s = [s stringByReplacingOccurrencesOfString:@"AndModifySelection" withString:@""];
|
||||
NSDictionary *commands = @{
|
||||
@"deleteBackward:" : @(UHDK_EDIT_DELETE_BACKWARD),
|
||||
@"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),
|
||||
@"moveBackward:" : @(UHDK_EDIT_MOVE_LEFT),
|
||||
@"moveBackward:" : @(UHDK_EDIT_MOVE_BACKWARD),
|
||||
@"moveRight:" : @(UHDK_EDIT_MOVE_RIGHT),
|
||||
@"moveForward:" : @(UHDK_EDIT_MOVE_RIGHT),
|
||||
@"moveForward:" : @(UHDK_EDIT_MOVE_FORWARD),
|
||||
@"moveUp:" : @(UHDK_EDIT_MOVE_UP),
|
||||
@"moveDown:" : @(UHDK_EDIT_MOVE_DOWN),
|
||||
@"moveToLeftEndOfLine:" : @(UHDK_EDIT_LINE_LEFT_END),
|
||||
@"moveToRightEndOfLine:" : @(UHDK_EDIT_LINE_RIGHT_END),
|
||||
@"moveToBeginningOfLine:" : @(UHDK_EDIT_LINE_START),
|
||||
@"moveToEndOfLine:" : @(UHDK_EDIT_LINE_END),
|
||||
@"moveToBeginningOfDocument:" : @(UHDK_EDIT_DOCUMENT_START),
|
||||
@"moveToEndOfDocument:" : @(UHDK_EDIT_DOCUMENT_END),
|
||||
@"moveWordLeft:" : @(UHDK_EDIT_WORD_LEFT),
|
||||
@"moveWordBackward:" : @(UHDK_EDIT_WORD_LEFT),
|
||||
@"moveWordBackward:" : @(UHDK_EDIT_WORD_BACKWARD),
|
||||
@"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),
|
||||
@"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),
|
||||
@"cut:" : @(UHDK_EDIT_CUT),
|
||||
@"paste:" : @(UHDK_EDIT_PASTE),
|
||||
|
|
@ -536,49 +667,20 @@ static uhdk_key logical(NSEvent *e) {
|
|||
@"redo:" : @(UHDK_EDIT_REDO)
|
||||
};
|
||||
NSNumber *command = commands[s];
|
||||
if (!command) {
|
||||
if ([s isEqualToString:@"cancelOperation:"])
|
||||
[self cancelMarked];
|
||||
if (!command)
|
||||
return;
|
||||
}
|
||||
[self editID];
|
||||
z_event e = {.type = UHDK_EVENT_EDIT_COMMAND};
|
||||
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];
|
||||
// Keep common edit operations coherent for subsequent input before an ack.
|
||||
NSRange r = self.selection;
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (ownInput)
|
||||
self.currentInput = 0;
|
||||
}
|
||||
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
|
||||
return ([sender.draggingPasteboard
|
||||
|
|
@ -618,6 +720,30 @@ static uhdk_key logical(NSEvent *e) {
|
|||
}
|
||||
@end
|
||||
@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 {
|
||||
if (!self.window || !self.view)
|
||||
return;
|
||||
|
|
@ -628,17 +754,23 @@ static uhdk_key logical(NSEvent *e) {
|
|||
CGLUnlockContext(gl);
|
||||
}
|
||||
NSRect bounds = self.view.bounds, pixels = [self.view convertRectToBacking:bounds];
|
||||
uhdk_window_metrics m = {.struct_size = sizeof(m),
|
||||
.logical_size = {bounds.size.width, bounds.size.height},
|
||||
.framebuffer_width = (uint32_t)MAX(0, pixels.size.width),
|
||||
.framebuffer_height = (uint32_t)MAX(0, pixels.size.height),
|
||||
.scale = self.window.backingScaleFactor,
|
||||
.focused = self.window.isKeyWindow,
|
||||
.visible = self.window.isVisible};
|
||||
m.state = self.window.isMiniaturized ? UHDK_WINDOW_MINIMIZED
|
||||
: (self.window.styleMask & NSWindowStyleMaskFullScreen) ? UHDK_WINDOW_FULLSCREEN
|
||||
: self.window.isZoomed ? UHDK_WINDOW_MAXIMIZED
|
||||
: UHDK_WINDOW_NORMAL;
|
||||
uhdk_window_metrics m = {
|
||||
.struct_size = sizeof(m),
|
||||
.logical_size = {bounds.size.width, bounds.size.height},
|
||||
.framebuffer_width = (uint32_t)MAX(0, pixels.size.width),
|
||||
.framebuffer_height = (uint32_t)MAX(0, pixels.size.height),
|
||||
.scale = self.window.backingScaleFactor,
|
||||
.focused = self.window.isKeyWindow,
|
||||
.visible = self.window.isVisible,
|
||||
.observations = UHDK_WINDOW_OBSERVE_MINIMIZED | UHDK_WINDOW_OBSERVE_RESIZING |
|
||||
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
|
||||
: UHDK_WINDOW_NORMAL;
|
||||
z_metrics(self.handle, &m);
|
||||
z_event e = {.type = UHDK_EVENT_WINDOW_REDRAW, .window = self.handle};
|
||||
z_emit(host.app, &e);
|
||||
|
|
@ -653,6 +785,10 @@ static uhdk_key logical(NSEvent *e) {
|
|||
(void)n;
|
||||
[self metrics];
|
||||
}
|
||||
- (void)windowDidChangeOcclusionState:(NSNotification *)n {
|
||||
(void)n;
|
||||
[self metrics];
|
||||
}
|
||||
- (void)windowDidChangeBackingProperties:(NSNotification *)n {
|
||||
(void)n;
|
||||
[self metrics];
|
||||
|
|
@ -665,6 +801,8 @@ static uhdk_key logical(NSEvent *e) {
|
|||
- (void)windowDidResignKey:(NSNotification *)n {
|
||||
(void)n;
|
||||
[self.view.pressedModifiers removeAllIndexes];
|
||||
[self.view.deferredInput removeAllObjects];
|
||||
self.view.awaitingSnapshot = NO;
|
||||
[self.view cancelMarked];
|
||||
if (self.view.capture) {
|
||||
self.view.capture = NO;
|
||||
|
|
@ -683,13 +821,34 @@ static uhdk_key logical(NSEvent *e) {
|
|||
(void)n;
|
||||
[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)n;
|
||||
self.fullscreenTransition = NO;
|
||||
[self metrics];
|
||||
[self applyPendingState];
|
||||
}
|
||||
- (void)windowDidExitFullScreen:(NSNotification *)n {
|
||||
(void)n;
|
||||
self.fullscreenTransition = NO;
|
||||
[self metrics];
|
||||
[self applyPendingState];
|
||||
}
|
||||
@end
|
||||
@interface ZMenuAction : NSObject
|
||||
|
|
@ -715,12 +874,11 @@ static uhdk_key logical(NSEvent *e) {
|
|||
e.type = UHDK_EVENT_QUIT_REQUESTED;
|
||||
e.window = NULL;
|
||||
} 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,
|
||||
UHDK_EDIT_SELECT_ALL, UHDK_EDIT_UNDO, UHDK_EDIT_REDO};
|
||||
e.type = UHDK_EVENT_EDIT_COMMAND;
|
||||
e.data.edit.command = edit[self.role - UHDK_MENU_ROLE_COPY];
|
||||
e.input_id = ++w.view.nextInput;
|
||||
w.view.lastEdit = e.input_id;
|
||||
static const char *selectors[] = {
|
||||
"copy:", "cut:", "paste:", "selectAll:", "undo:", "redo:"
|
||||
};
|
||||
[w.view doCommandBySelector:sel_registerName(selectors[self.role - UHDK_MENU_ROLE_COPY])];
|
||||
return;
|
||||
}
|
||||
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 metrics];
|
||||
break;
|
||||
case Z_STATE: {
|
||||
BOOL full = (w.window.styleMask & NSWindowStyleMaskFullScreen) != 0;
|
||||
if ((cmd->number == UHDK_WINDOW_FULLSCREEN) != full)
|
||||
[w.window toggleFullScreen:nil];
|
||||
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];
|
||||
case Z_STATE:
|
||||
w.pendingState = (uint32_t)cmd->number;
|
||||
w.hasPendingState = YES;
|
||||
[w applyPendingState];
|
||||
break;
|
||||
}
|
||||
case Z_VISIBLE:
|
||||
if (cmd->number)
|
||||
[w.window orderFront:nil];
|
||||
|
|
@ -1436,8 +1584,11 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
|
|||
break;
|
||||
case Z_TEXT_ENABLE:
|
||||
w.view.textEnabled = cmd->number != 0;
|
||||
if (!cmd->number)
|
||||
if (!cmd->number) {
|
||||
[w.view.deferredInput removeAllObjects];
|
||||
w.view.awaitingSnapshot = NO;
|
||||
[w.view cancelMarked];
|
||||
}
|
||||
break;
|
||||
case Z_TEXT_STATE:
|
||||
[w.view applyState:&cmd->text_state];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#define UHDK_ENABLE_NATIVE
|
||||
#include "uhdk.h"
|
||||
#include <stddef.h>
|
||||
#include "edit_keys.h"
|
||||
/* Private producer storage. Converted to public pointer payloads at poll. */
|
||||
typedef struct z_request_result {
|
||||
uhdk_request_id id;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ struct Window {
|
|||
uint32_t flags;
|
||||
uhdk_size min_size, max_size;
|
||||
float scale;
|
||||
BOOL text_enabled, tracking, fullscreen, composing, suppress_char;
|
||||
BOOL text_enabled, tracking, fullscreen, composing, suppress_char, resizing;
|
||||
WCHAR high_surrogate;
|
||||
uint64_t input_id, last_edit;
|
||||
uhdk_text_state text_state;
|
||||
|
|
@ -180,10 +180,16 @@ static void metrics(Window *w) {
|
|||
m.framebuffer_width = (uint32_t)(r.right - r.left);
|
||||
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.state = w->fullscreen ? UHDK_WINDOW_FULLSCREEN
|
||||
: IsIconic(w->hwnd) ? UHDK_WINDOW_MINIMIZED
|
||||
: IsZoomed(w->hwnd) ? UHDK_WINDOW_MAXIMIZED
|
||||
: UHDK_WINDOW_NORMAL;
|
||||
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
|
||||
: (IsZoomed(w->hwnd) || (m.minimized && (placement.flags & WPF_RESTORETOMAXIMIZED)))
|
||||
? UHDK_WINDOW_MAXIMIZED
|
||||
: UHDK_WINDOW_NORMAL;
|
||||
m.focused = GetFocus() == w->hwnd;
|
||||
m.visible = IsWindowVisible(w->hwnd);
|
||||
z_metrics(w->handle, &m);
|
||||
|
|
@ -228,13 +234,15 @@ static void textEvent(Window *w, uint32_t type, const WCHAR *text, int length, i
|
|||
profileFree(s);
|
||||
}
|
||||
static void editEvent(Window *w, uint32_t command) {
|
||||
if (w->composing)
|
||||
return;
|
||||
z_event e = {0};
|
||||
e.type = UHDK_EVENT_EDIT_COMMAND;
|
||||
e.window = w->handle;
|
||||
e.input_id = w->input_id = ++host.input_id;
|
||||
w->last_edit = e.input_id;
|
||||
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);
|
||||
}
|
||||
static void menuEvent(Window *w, const uhdk_menu_item *item) {
|
||||
|
|
@ -366,29 +374,6 @@ static uint32_t physicalKey(LPARAM param) {
|
|||
}
|
||||
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) {
|
||||
HIMC ime = ImmGetContext(w->hwnd);
|
||||
if (!ime)
|
||||
|
|
@ -497,6 +482,14 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||
}
|
||||
case WM_ERASEBKGND:
|
||||
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_SHOWWINDOW:
|
||||
metrics(w);
|
||||
|
|
@ -657,7 +650,7 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||
w->suppress_char = FALSE;
|
||||
const uhdk_menu_item *item =
|
||||
shortcut(w->menu_source ? w->menu_source : host.menu, key, mods & 15);
|
||||
if (item) {
|
||||
if (item && !w->composing) {
|
||||
w->suppress_char = TRUE;
|
||||
menuEvent(w, item);
|
||||
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));
|
||||
z_emit(host.app, &e);
|
||||
if (down && w->text_enabled && !w->composing) {
|
||||
uint32_t command = editKey(wp, mods);
|
||||
if (command)
|
||||
uint32_t command = z_edit_key(key, mods);
|
||||
if (command) {
|
||||
w->suppress_char = TRUE;
|
||||
editEvent(w, command);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -686,14 +682,8 @@ static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|||
return TRUE;
|
||||
if (!w->text_enabled || w->composing || w->suppress_char)
|
||||
return 0;
|
||||
if (wp == '\r') {
|
||||
editEvent(w, UHDK_EDIT_NEWLINE);
|
||||
return 0;
|
||||
}
|
||||
if (wp == '\t') {
|
||||
editEvent(w, UHDK_EDIT_TAB);
|
||||
return 0;
|
||||
}
|
||||
// Enter/Tab are semantic keydown commands. Ignore their translated
|
||||
// WM_CHAR messages even if another queued key changed suppress_char.
|
||||
if (wp < 32 || wp == 127)
|
||||
return 0;
|
||||
WCHAR chars[2];
|
||||
|
|
@ -1703,8 +1693,11 @@ uhdk_status z_native_execute(uhdk_app *app, const z_command *cmd, uint64_t reque
|
|||
break;
|
||||
}
|
||||
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 (IsIconic(w->hwnd))
|
||||
ShowWindow(w->hwnd, SW_RESTORE);
|
||||
w->placement.length = sizeof(w->placement);
|
||||
GetWindowPlacement(w->hwnd, &w->placement);
|
||||
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,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||||
}
|
||||
if (!full)
|
||||
ShowWindow(w->hwnd, cmd->number == UHDK_WINDOW_MINIMIZED ? SW_MINIMIZE
|
||||
: cmd->number == UHDK_WINDOW_MAXIMIZED ? SW_MAXIMIZE
|
||||
: SW_RESTORE);
|
||||
if (minimize)
|
||||
ShowWindow(w->hwnd, SW_MINIMIZE);
|
||||
else if (!full)
|
||||
ShowWindow(w->hwnd, cmd->number == UHDK_WINDOW_MAXIMIZED ? SW_MAXIMIZE : SW_RESTORE);
|
||||
else if (IsIconic(w->hwnd))
|
||||
ShowWindow(w->hwnd, SW_RESTORE);
|
||||
metrics(w);
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue