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
|
#pragma once
#include <algorithm>
#include <cstdint>
// Pure decision core 1 — the REVEAL recognizer (GLOSSARY "reveal"/"swipe"): the
// reversible, finger-following recognizer for the left-edge swipe that shows the
// stage dock. No wlroots / GL / RMLUi — input is a stream of drag samples (layout
// x/y + time ms), output is a reveal FRACTION in [0,1] and, on release, a commit
// decision (snap fully open or fully closed). Heavily doctest-covered in
// tests/test_policy.cpp with nothing running; the glue (e1) feeds kernel touch/
// pointer events into these calls and animates from the result.
//
// Reversibility: dragging inward (rightward, away from the left edge) GROWS the
// fraction; dragging back toward the edge SHRINKS it. So the same recognizer
// drives the close gesture too — see RevealRecognizer::begin's start_fraction.
//
// Single wl_event_loop thread throughout (no internal synchronization).
namespace unbox::ext_stage_dock::reveal {
// Tunables for the recognizer. `dock_width` is the inward travel (px) that maps
// to a full reveal (fraction 1). `open_threshold` is the release fraction at or
// above which a slow drag commits OPEN. `fling_velocity` (px/ms, inward
// positive) is the release speed at or above which a fast flick commits OPEN
// even below the threshold (and, symmetrically, an outward flick at or below
// -fling_velocity commits CLOSE even above the threshold). `edge_slop` is how
// far from the left edge (px) a press may start and still count as a reveal.
struct RevealConfig {
int dock_width = 320;
double open_threshold = 0.5;
double fling_velocity = 1.0; // px/ms
int edge_slop = 24; // px from the left edge
};
// The release decision: snap the dock fully open or fully closed.
enum class RevealCommit {
open,
close,
};
// A reversible finger-following recognizer for ONE drag. Construct with config,
// begin() at the down sample, update() on each move (returns the live fraction),
// end() at the up sample (returns the commit). Reusable across drags: a fresh
// begin() resets all per-drag state.
//
// Geometry note: only the X axis matters (the dock slides horizontally); Y is
// accepted for symmetry with the kernel touch/pointer payloads and ignored. All
// fractions are clamped to [0,1].
class RevealRecognizer {
public:
explicit RevealRecognizer(RevealConfig config) : config_(config) {}
// Begin a drag at the down sample (layout x/y, time ms). Returns true iff
// this is a reveal gesture — i.e. it started within `edge_slop` of the left
// edge (x <= edge_slop). When false, the caller should ignore the gesture
// (it is not an edge swipe) and NOT feed update()/end().
//
// start_fraction seeds the fraction this drag begins from, so a CLOSE drag
// (starting from the already-open dock) reuses the SAME recognizer: pass
// start_fraction = 1.0 and a start x at the dock's right edge, then dragging
// back toward the screen edge shrinks the fraction toward 0 and end() can
// commit close. For the normal OPEN-from-hidden gesture, leave it 0.0. The
// anchor x (origin_x_) is the down x; the fraction tracks inward travel from
// there, biased by start_fraction.
//
// force_active SKIPS the edge-slop gate (the `x <= edge_slop` test) and makes
// the gesture active regardless of where it began. The OPEN gesture leaves it
// false: it must start within edge_slop of the screen edge to count as a
// reveal. The CLOSE gesture (e1, fed by UiSurface::bind_drag) sets it true:
// the finger lands ANYWHERE on the already-open dock (surface-local x is in
// [0, dock_width], not near the edge), so the edge gate would wrongly reject
// it. The travel math (fraction = start_fraction + (x - origin_x)/width) is
// origin-relative, so a forced-active close still tracks the finger correctly
// from whatever x it began at. Velocity / fling are unaffected.
auto begin(double x, double y, std::uint32_t t, double start_fraction = 0.0,
bool force_active = false) -> bool {
(void)y;
origin_x_ = x;
start_fraction_ = clamp_fraction(start_fraction);
fraction_ = start_fraction_;
last_x_ = x;
last_t_ = t;
velocity_ = 0.0;
active_ = force_active || x <= static_cast<double>(config_.edge_slop);
return active_;
}
// Update with the current sample; returns the live reveal fraction in [0,1].
// The fraction is start_fraction + (inward travel from origin / dock_width),
// clamped. Inward = rightward (x increasing) for the open gesture; dragging
// back toward the edge decreases it (fully reversible). Also tracks a
// smoothed-enough instantaneous velocity (px/ms, inward positive) for the
// fling decision in end(). A non-active recognizer (begin returned false)
// returns its current fraction unchanged.
auto update(double x, double y, std::uint32_t t) -> double {
(void)y;
if (!active_) {
return fraction_;
}
const double inward = x - origin_x_;
const double width = config_.dock_width > 0 ? static_cast<double>(config_.dock_width) : 1.0;
fraction_ = clamp_fraction(start_fraction_ + inward / width);
// Instantaneous velocity from the last sample. dt==0 (same timestamp)
// keeps the previous velocity rather than dividing by zero.
const double dt = static_cast<double>(t) - static_cast<double>(last_t_);
if (dt > 0.0) {
velocity_ = (x - last_x_) / dt;
}
last_x_ = x;
last_t_ = t;
return fraction_;
}
// Release; decide open vs close from the final fraction + recent velocity:
// * a fast INWARD fling (velocity >= fling_velocity) commits OPEN even
// below the threshold;
// * a fast OUTWARD fling (velocity <= -fling_velocity) commits CLOSE even
// above the threshold;
// * otherwise a slow release commits by position: OPEN iff
// fraction >= open_threshold, else CLOSE.
// `t` is accepted for symmetry / a final velocity refresh but the decision
// uses the velocity accumulated across update()s. A non-active recognizer
// commits close (nothing was revealed).
auto end(std::uint32_t t) -> RevealCommit {
(void)t;
if (!active_) {
return RevealCommit::close;
}
if (velocity_ >= config_.fling_velocity) {
return RevealCommit::open;
}
if (velocity_ <= -config_.fling_velocity) {
return RevealCommit::close;
}
return fraction_ >= config_.open_threshold ? RevealCommit::open : RevealCommit::close;
}
// The live fraction (same value the last update() returned). Read-only.
[[nodiscard]] auto fraction() const -> double { return fraction_; }
// Whether begin() accepted this drag as an edge-started reveal.
[[nodiscard]] auto active() const -> bool { return active_; }
// The last measured instantaneous velocity (px/ms, inward positive).
[[nodiscard]] auto velocity() const -> double { return velocity_; }
private:
[[nodiscard]] static auto clamp_fraction(double f) -> double {
return std::clamp(f, 0.0, 1.0);
}
RevealConfig config_;
double origin_x_ = 0.0;
double start_fraction_ = 0.0;
double fraction_ = 0.0;
double last_x_ = 0.0;
std::uint32_t last_t_ = 0;
double velocity_ = 0.0;
bool active_ = false;
};
} // namespace unbox::ext_stage_dock::reveal
|