macos inputs

This commit is contained in:
Peter Li 2026-09-18 16:30:35 -07:00
parent 85acfb5b67
commit c0d923cbed
17 changed files with 862 additions and 187 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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));
}

100
src/edit_keys.h Normal file
View File

@ -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

View File

@ -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;
}

View File

@ -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));

View File

@ -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;

View File

@ -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

View File

@ -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;

60
src/linux/WindowState.zig Normal file
View File

@ -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);
}

View File

@ -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 => {

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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];

View File

@ -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;

View File

@ -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;
}