morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
browser_history.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4/// @file
5/// @brief `BrowserHistory`: the browser's session history as an `ui::IHistory`, on WebAssembly only (spec 3 §10).
6///
7/// Specified in `docs/spec/ui/host.md`, "Route history".
8
9#ifdef __EMSCRIPTEN__
10
11#include <emscripten/val.h>
12
13#include <QObject>
14#include <QTimer>
15#include <chrono>
16#include <functional>
17#include <string>
18#include <utility>
19
21
22namespace morph::qt_quick {
23
24/// @brief The page's history: each entry's route is its URL fragment, `#/<route>`.
25///
26/// `push` and `replace` go through `history.pushState` and `history.replaceState`, which change the fragment without
27/// a `hashchange`. A move back or forward, or an address the user edited, changes the fragment from outside; it is
28/// found by reading `location.hash` on a timer on the Qt event loop, so `onPop`'s callback runs there, never inside
29/// a JavaScript event handler. Reading it costs one property read per tick; a `popstate` listener would need a
30/// JavaScript function bound to C++, which a header cannot register.
31class BrowserHistory final : public ui::IHistory {
32public:
33 /// @brief The page's history, read every @p interval.
34 /// @param interval How often the fragment is read.
35 explicit BrowserHistory(std::chrono::milliseconds interval = std::chrono::milliseconds{100}) : _seen{hash()} {
36 _timer.setInterval(interval);
37 QObject::connect(&_timer, &QTimer::timeout, [this] { poll(); });
38 _timer.start();
39 }
40
41 [[nodiscard]] std::string current() const override { return ui::routeOfFragment(hash()); }
42
43 void push(std::string const& route) override { write("pushState", route); }
44
45 void replace(std::string const& route) override { write("replaceState", route); }
46
47 void onPop(std::function<void(std::string const& route)> popped) override { _popped = std::move(popped); }
48
49private:
50 [[nodiscard]] static std::string hash() { return emscripten::val::global("location")["hash"].as<std::string>(); }
51
52 // Skipped while the address holds a move the timer has not reported yet: the user's move wins, and the poll
53 // reports it.
54 void write(char const* method, std::string const& route) {
55 if (hash() != _seen) {
56 return;
57 }
58 emscripten::val::global("history").call<void>(method, emscripten::val::null(), std::string{},
59 ui::fragmentOf(route));
60 _seen = hash();
61 }
62
63 // A copy of the callback: it may replace it while it runs.
64 void poll() {
65 std::string now = hash();
66 if (now == _seen) {
67 return;
68 }
69 _seen = now;
70 if (auto const popped = _popped) {
71 popped(ui::routeOfFragment(now));
72 }
73 }
74
75 QTimer _timer;
76 std::string _seen;
77 std::function<void(std::string const&)> _popped;
78};
79
80} // namespace morph::qt_quick
81
82#endif
Route history (spec 3 §10, spec 6 §20): the route of the screen a shell shows, kept in a history the ...