1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
#pragma once
#include <cstdint>
// Pure decision core (no wlroots / GL / RMLUi). The glue translates wlroots
// input into these calls and acts on the results. Heavily doctest-covered in
// tests/test_policy.cpp without the kernel present. This file calls nothing in
// the glue — it only computes.
//
// Compositor keybindings (Ctrl+Alt+Backspace terminate, Alt+F1 focus-cycle)
// have been removed from this unit; they now live in ext-keybindings.
namespace unbox::ext_xdg_shell::policy {
// ---- Interactive move/resize grab state machine (pure) ----------------------
//
// Root-causes the user-observed bug: "dragging a titlebar doesn't move the
// window while the button is held, but after releasing, unclicked motion drags
// it." That symptom is a grab whose lifetime is decoupled from the button: the
// grab outlives the press (so post-release motion still moves) and/or is not
// actually engaged while the button is down.
//
// The fix is to make the grab a deterministic function of two facts the glue
// feeds in — whether the pointer button is currently DOWN, and whether the
// client has requested an interactive move/resize — and to gate every action on
// them. Invariants this machine guarantees:
// * A grab is ACTIVE iff a move/resize was requested AND the button is still
// down. Motion moves/resizes the toplevel ONLY while active (and the glue
// suppresses the client pointer notify then).
// * A button RELEASE always ends the grab and restores passthrough — no
// matter when the request arrived relative to the press/release.
// * A move/resize request that arrives while NO button is down does NOT
// engage a grab (so a stale/late request can never start an unclicked drag).
// This is pure logic; the glue calls process_cursor_move/resize and the
// seat/cursor effects when ask() says so.
enum class GrabMode { none, move, resize };
// What the glue should DO after feeding an event in.
enum class GrabAction {
none, // nothing to do
move_toplevel, // run process_cursor_move (motion while move-grabbing)
resize_toplevel, // run process_cursor_resize (motion while resize-grabbing)
end_grab, // grab ended: reset cursor mode + restore default cursor
};
// A grab is driven by EITHER the pointer button OR a single touch point. The
// machine tracks which inputs are currently down, and — once a client
// move/resize request engages a grab — pins the grab to ONE originating source:
// * pointer: ended by the pointer button release;
// * a specific touch id: ended by THAT point's up/cancel only.
// A second simultaneous touch point never steers or ends the grab (the grab
// follows only its originating point — the simplest honest rule). Pointer and
// touch are isolated: pointer motion never moves a touch-driven grab and vice
// versa.
class GrabMachine {
public:
[[nodiscard]] auto mode() const -> GrabMode { return mode_; }
[[nodiscard]] auto grabbing() const -> bool { return mode_ != GrabMode::none; }
// True iff the pointer button is currently held (preserved pointer API).
[[nodiscard]] auto button_down() const -> bool { return pointer_down_; }
// True iff the grab is currently driven by a touch point (else pointer or
// not grabbing).
[[nodiscard]] auto touch_driven() const -> bool {
return mode_ != GrabMode::none && source_ == Source::touch;
}
// ---- pointer source ----
// The pointer button went down/up. A release ALWAYS tears down a grab that
// the POINTER is driving (a touch-driven grab is unaffected).
auto on_button(bool pressed) -> GrabAction {
pointer_down_ = pressed;
if (!pressed && mode_ != GrabMode::none && source_ == Source::pointer) {
return reset();
}
return GrabAction::none;
}
// ---- touch source ----
// A touch point went down / up / cancelled. Up or cancel of the
// grab-ORIGINATING point ends the grab; any other point is ignored by the
// grab machine.
void on_touch_down(std::int32_t touch_id) {
++touch_down_count_;
last_touch_id_ = touch_id;
any_touch_id_valid_ = true;
}
auto on_touch_up(std::int32_t touch_id) -> GrabAction { return touch_lifted(touch_id); }
auto on_touch_cancel(std::int32_t touch_id) -> GrabAction { return touch_lifted(touch_id); }
// ---- engage ----
// The client requested an interactive move/resize. Engages only while an
// input is actually down, pinning the grab to that source (touch preferred
// when a touch point is down — touch CSD drag has no pointer button; a
// request with NOTHING down is ignored, preventing an unclicked drag).
auto on_request_move() -> bool { return engage(GrabMode::move); }
auto on_request_resize() -> bool { return engage(GrabMode::resize); }
// ---- motion ----
// Pointer motion: acts only on a pointer-driven grab.
[[nodiscard]] auto on_motion() const -> GrabAction {
return source_ == Source::pointer ? motion_action() : GrabAction::none;
}
// Touch motion of `touch_id`: acts only on a grab driven by THAT point.
[[nodiscard]] auto on_touch_motion(std::int32_t touch_id) const -> GrabAction {
return (source_ == Source::touch && mode_ != GrabMode::none && touch_id == origin_touch_id_)
? motion_action()
: GrabAction::none;
}
// The grabbed toplevel went away (unmap/destroy): drop the grab silently.
void on_grab_target_lost() {
mode_ = GrabMode::none;
source_ = Source::pointer;
}
private:
enum class Source { pointer, touch };
[[nodiscard]] auto motion_action() const -> GrabAction {
switch (mode_) {
case GrabMode::move:
return GrabAction::move_toplevel;
case GrabMode::resize:
return GrabAction::resize_toplevel;
case GrabMode::none:
return GrabAction::none;
}
return GrabAction::none;
}
auto engage(GrabMode mode) -> bool {
// Touch preferred when a point is down (CSD touch drag has no pointer
// button); else the pointer; else nothing-down -> ignore the request.
if (touch_down_count_ > 0 && any_touch_id_valid_) {
source_ = Source::touch;
origin_touch_id_ = last_touch_id_;
} else if (pointer_down_) {
source_ = Source::pointer;
} else {
return false;
}
mode_ = mode;
return true;
}
auto touch_lifted(std::int32_t touch_id) -> GrabAction {
if (touch_down_count_ > 0) {
--touch_down_count_;
}
if (mode_ != GrabMode::none && source_ == Source::touch && touch_id == origin_touch_id_) {
return reset();
}
return GrabAction::none;
}
auto reset() -> GrabAction {
mode_ = GrabMode::none;
source_ = Source::pointer;
return GrabAction::end_grab;
}
GrabMode mode_ = GrabMode::none;
Source source_ = Source::pointer;
// Pointer input state.
bool pointer_down_ = false;
// Touch input state. We only need to know whether ANY point is down (to
// gate engage) and the originating point's id (to steer/end the grab); the
// brief's "second point must not confuse the grab" is satisfied by pinning
// to origin_touch_id_ and ignoring all others.
int touch_down_count_ = 0;
bool any_touch_id_valid_ = false;
std::int32_t last_touch_id_ = 0;
std::int32_t origin_touch_id_ = 0;
};
} // namespace unbox::ext_xdg_shell::policy
|