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
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT
//! A stop request any thread may make, read by the loop's own frame tick.
//!
//! # The problem this solves
//!
//! A host that does not drive the loop itself — a smoke test, CI, a scripted demo — has to be
//! able to *end* a session, or its only clean exit is `std::process::exit`, which makes "the
//! session ended" and "the process was killed" indistinguishable in the exit code and the log.
//!
//! No toolkit lets an arbitrary thread stop its loop: `-[NSApplication stop:]` is
//! main-thread-only, GTK's `gtk_main_quit` walks a global main-loop stack that only the loop's
//! thread may touch, and a Win32 `PostQuitMessage` is thread-affine. So the request cannot be
//! *performed* from the watcher thread. What it can be is **recorded**: this module holds one
//! atomic flag, and the loop reads it.
//!
//! # Why the frame tick is the reader
//!
//! Every loop this library owns calls [`crate::drive_frame`] once per iteration **on the
//! thread that owns the loop**: GTK's `glib::timeout_add_local` closure, the polling loops of
//! the state/wayland/windows/mobile/harmony backends, and — the case that motivated this — the
//! repeating `NSTimer` AppKit installs with each window. That makes the frame tick the one
//! seam that exists in every backend *and* runs where the toolkit permits a stop, so the check
//! lives there rather than in each of the eleven loops (principles #36/#54: one implementation,
//! and no OS knowledge outside `src/platform/`).
//!
//! The recorded request therefore turns "the watcher thread cannot stop the loop" into "the
//! watcher thread asks, and the loop stops itself at its next frame" — without any cross-thread
//! toolkit call.
//!
//! # What `loop_has_ticked` answers, and what it does not
//!
//! A request made while no loop is running has no reader: nothing is ticking, so nothing will
//! read it. [`request`] reports that fact by returning whether a loop has driven a frame in
//! this process. It is deliberately the *observed* fact and not a per-backend claim: a backend
//! whose loop never runs (a state-only host, `mini`/`embedded`) has not ticked and answers
//! `false`, which is what a caller needs to decide whether to arrange its own ending.
//!
//! The request is **not** discarded in that case. It stays recorded, so a loop that starts
//! afterwards reads it at its first frame — the same "asked before it was running" case a host
//! hits when it calls this between `create_window` and `run`.
use crate;
/// A stop has been asked for and no loop has read it yet.
static STOP_REQUESTED: AtomicBool = new;
/// Whether any loop in this process has driven a frame.
///
/// Set by [`honour_request_on_loop_thread`], which is the frame tick itself, so it is true
/// exactly when a loop has reached its frame body at least once. Read by [`request`] to answer
/// "is there a reader for this?".
static LOOP_TICKED: AtomicBool = new;
/// Records a stop request and reports whether a loop is in a position to read it.
///
/// Returns `true` when a loop has driven a frame in this process: that loop reads the request
/// at its next frame and stops. Returns `false` when no loop has ticked, so nothing is reading
/// the request right now — the request stays recorded and a loop started later honours it at
/// its first frame.
///
/// This is the answer the public [`crate::quit`] reports (the entry a host calls), and the
/// reason it can be asked from a thread the toolkit would refuse: recording is one atomic
/// store, which every thread may do. Performing the toolkit's own stop is a *separate*,
/// thread-bound half — [`Platform::quit`](crate::platform::Platform::quit) — which the frame
/// tick above calls on the loop's own thread.
/// Returns whether a stop request is currently recorded and unread.
///
/// A read-only query: it does not consume the request. The loop's own path uses
/// [`take`] instead, because reading the request is what acknowledges it.
/// Returns whether any loop in this process has driven a frame.
///
/// See [`request`] for why this is an observation rather than a per-backend claim. It is true
/// from the first frame of the first loop and stays true for the rest of the process: a loop
/// that has run is evidence that this profile's loops do run, which is the fact a caller needs
/// before relying on a cross-thread stop.
/// Consumes a recorded request, reporting whether there was one.
///
/// Taking rather than reading is what makes a request stop exactly one loop: a host that ends
/// a session and then opens another one in the same process does not have the second session
/// stopped by the first session's request.
/// Marks this thread as the frame tick and stops the loop if a request is pending.
///
/// Called by [`crate::drive_frame`] on the loop's own thread, which is the only thread a
/// toolkit accepts a stop from. The order inside is deliberate: the tick is recorded *before*
/// the request is taken, so a host that asks from another thread while this frame is running
/// is answered by the next frame rather than by a frame that has already passed the check.
///
/// # Why the stop is delegated to the backend
///
/// Stopping a loop is per-toolkit work (`gtk_main_quit`, `NSApplication stop:`,
/// `PostQuitMessage`), and each backend already answers that in its own
/// [`Platform::quit`](crate::platform::Platform::quit). Calling it from here — where we are
/// provably on the loop's thread — is what keeps the toolkit rules inside the backends: this
/// module knows about one atomic flag and nothing about any toolkit.
pub