summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/ui_core.hpp
blob: e7c0723293b9c1e37923d3c9f542d88f2a90ce05 (plain)
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
#pragma once

#include <cstdint>

// Pure decision cores for the ui substrate — NO wlroots / GL / RMLUi types, so
// they are doctest-able with nothing running (AGENTS.md: effects at the edges,
// pure cores tested hard). The substrate glue (ui_substrate.cpp) injects the
// effects around these.
//
// Everything runs on the single wl_event_loop thread; no synchronization here.

namespace unbox::kernel {

// touch-mode: the substrate-level theme state that scales hit targets for
// finger input. It flips automatically by last-input kind — a touch event
// turns it ON, pointer motion turns it OFF — but with debounce so a stray
// pointer jitter during a touch interaction (palm, accidental trackpad brush)
// does not flicker it. A manual override pins it for tests/config.
//
// Pure state machine: feed it input-kind events + a monotonic timestamp; it
// returns whether the effective mode CHANGED so the caller can re-theme only
// on a transition. The debounce rule: after a touch event, pointer motion is
// ignored for `debounce_ms`; a touch event always wins immediately.
class TouchModeTracker {
public:
    enum class Mode { pointer, touch };
    enum class Override { none, force_pointer, force_touch };

    explicit TouchModeTracker(std::uint32_t debounce_ms = 700) : debounce_ms_(debounce_ms) {}

    // A touch event happened at time_msec. Returns true if the EFFECTIVE mode
    // changed. Touch always wins immediately and arms the debounce window.
    auto on_touch(std::uint32_t time_msec) -> bool {
        last_touch_msec_ = time_msec;
        have_touch_ = true;
        return set_auto(Mode::touch);
    }

    // Pointer motion at time_msec. Ignored (does NOT flip to pointer) while
    // within debounce_ms of the last touch — that suppresses palm/jitter.
    // Returns true if the effective mode changed.
    auto on_pointer_motion(std::uint32_t time_msec) -> bool {
        if (have_touch_ && time_msec - last_touch_msec_ < debounce_ms_) {
            return false; // inside the debounce shadow of a touch
        }
        return set_auto(Mode::pointer);
    }

    // Pin the mode regardless of input (tests/config). Override::none returns
    // to automatic, adopting the current auto-derived mode. Returns true if the
    // effective mode changed.
    auto set_override(Override ov) -> bool {
        const Mode before = effective();
        override_ = ov;
        return effective() != before;
    }

    [[nodiscard]] auto effective() const -> Mode {
        switch (override_) {
        case Override::force_pointer: return Mode::pointer;
        case Override::force_touch: return Mode::touch;
        case Override::none: break;
        }
        return auto_mode_;
    }

    [[nodiscard]] auto is_touch() const -> bool { return effective() == Mode::touch; }

private:
    auto set_auto(Mode m) -> bool {
        const Mode before = effective();
        auto_mode_ = m;
        return effective() != before;
    }

    std::uint32_t debounce_ms_;
    std::uint32_t last_touch_msec_ = 0;
    bool have_touch_ = false;
    Mode auto_mode_ = Mode::pointer;
    Override override_ = Override::none;
};

// NOTE (user decision, slice-5 hands-on): touch-mode causes NO automatic visual
// scaling. The dp-ratio knob is retired — the substrate leaves every context at
// RmlUi's default 1.0 permanently, so `dp` behaves like `px` in practice. The
// touch-mode STATE (auto-flip + debounce + on_touch_mode_changed notification)
// stays meaningful for invisible affordances and later slices (OSK auto-show,
// spacing); an extension that wants to adapt does so itself via the
// notification. (Earlier slices applied a 1.0/1.25 ratio here.)

// Who owns an in-flight pointer/touch grab — the consumer of the initiating
// press/down owns the whole stream until it ends (standard seat implicit-grab
// behavior). `none` = no grab active.
enum class GrabOwner { none, substrate, bus };

// Pure implicit-grab state for the pointer button stream. A grab begins on the
// FIRST button press (when no button was down) and ends when the LAST button
// is released; the owner is decided ONCE at grab start by whether the press
// landed over a ui surface, and EVERY event until the grab ends routes to that
// owner — regardless of what the cursor is over later. This is what makes a
// release land on the same party as its press (the slice-5 stuck-drag bug:
// press→extensions, release-over-ui-surface must still reach extensions).
class PointerButtonGrab {
public:
    // A button press landed; `over_surface` is the hit-test AT PRESS TIME.
    // Returns the owner this press (and the rest of the grab) routes to.
    auto press(bool over_surface) -> GrabOwner {
        if (down_count_ == 0) {
            owner_ = over_surface ? GrabOwner::substrate : GrabOwner::bus;
        }
        ++down_count_;
        return owner_;
    }
    // A button release. Returns the owner it routes to (the grab's owner). The
    // grab ends (owner -> none) when the last button comes up.
    auto release() -> GrabOwner {
        const GrabOwner who = owner_;
        if (down_count_ > 0 && --down_count_ == 0) {
            owner_ = GrabOwner::none;
        }
        return who;
    }
    [[nodiscard]] auto owner() const -> GrabOwner { return owner_; }
    [[nodiscard]] auto active() const -> bool { return down_count_ > 0; }

private:
    int down_count_ = 0;
    GrabOwner owner_ = GrabOwner::none;
};

// Axis-aligned hit test in layout coordinates: is (lx,ly) inside the rect at
// (x,y) of size w×h? Half-open on the far edges (matches scene node bounds).
[[nodiscard]] constexpr auto point_in_rect(double lx, double ly, int x, int y, int w, int h)
    -> bool {
    return lx >= x && ly >= y && lx < static_cast<double>(x) + w &&
           ly < static_cast<double>(y) + h;
}

} // namespace unbox::kernel