morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
route_history.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <functional>
5#include <memory>
6#include <string>
7#include <string_view>
8#include <utility>
9
10/// @file
11/// @brief Route history (spec 3 §10, spec 6 §20): the route of the screen a shell shows, kept in a history the user
12/// moves through with back and forward, such as a browser's.
13///
14/// Specified in `docs/spec/ui/host.md`, "Route history".
15
16namespace morph::ui {
17
18/// @brief A history of routes: a browser's session history, or a test's.
19class IHistory {
20public:
21 IHistory() = default;
22 IHistory(IHistory const&) = delete;
23 IHistory& operator=(IHistory const&) = delete;
24 IHistory(IHistory&&) = delete;
25 IHistory& operator=(IHistory&&) = delete;
26 virtual ~IHistory() = default;
27
28 /// @brief The route the current entry holds.
29 /// @return It, or empty when the entry holds none.
30 [[nodiscard]] virtual std::string current() const = 0;
31
32 /// @brief Adds an entry after the current one, dropping any entries forward of it, and makes it current.
33 /// @param route The route it holds.
34 virtual void push(std::string const& route) = 0;
35
36 /// @brief Replaces the route the current entry holds.
37 /// @param route The route.
38 virtual void replace(std::string const& route) = 0;
39
40 /// @brief Sets what is called when the current entry changes from outside: back, forward, or an address the user
41 /// edited. Replaces the previous callback; an empty one stops the calls.
42 /// @param popped Called with the new current entry's route, empty when it holds none.
43 virtual void onPop(std::function<void(std::string const& route)> popped) = 0;
44};
45
46/// @brief The URL fragment that holds a route.
47/// @param route The route.
48/// @return `#/` followed by the route.
49[[nodiscard]] inline std::string fragmentOf(std::string_view route) {
50 std::string fragment{"#/"};
51 fragment += route;
52 return fragment;
53}
54
55/// @brief The route a URL fragment holds.
56/// @param fragment The fragment, with its `#`.
57/// @return The route after `#/`, or empty for a fragment of any other form.
58[[nodiscard]] inline std::string routeOfFragment(std::string_view fragment) {
59 constexpr std::string_view prefix{"#/"};
60 return fragment.starts_with(prefix) ? std::string{fragment.substr(prefix.size())} : std::string{};
61}
62
63/// @brief Keeps the route of the screen a shell shows in a history, and opens the route of an entry the user moves
64/// to (spec 3 §10).
65///
66/// The shell opens `initial()` first, when it can, and calls `shown` after it mounts each screen. The first `shown`
67/// replaces the entry the history started on, so opening the first screen does not add a step back to an empty page;
68/// each later call adds an entry, unless the route is already the current entry's. Moving back or forward calls
69/// `open`, which shows the route's screen, or the start screen when the route cannot be opened, before it returns.
70/// The first screen it shows replaces the entry instead of adding one, so a refused route is not left in the history;
71/// a screen shown after that, such as a redirect, adds an entry as it would anywhere else.
73public:
74 /// @brief A history over @p history, which it keeps and follows from now on.
75 /// @param history The history. Borrowed: it outlives this.
76 /// @param open Shows the screen of a route the user moved to, calling `shown` before it returns. It may destroy
77 /// this.
78 RouteHistory(IHistory& history, std::function<void(std::string const& route)> open)
79 : _history{&history}, _open{std::move(open)}, _initial{history.current()} {
80 _history->onPop([this, alive = std::weak_ptr<bool const>{_alive}](std::string const& route) {
81 if (alive.expired()) {
82 return;
83 }
84 bool const outer = std::exchange(_replaceNext, true);
85 auto const restore = [&] {
86 if (!alive.expired()) {
87 _replaceNext = outer;
88 }
89 };
90 try {
91 _open(route);
92 } catch (...) {
93 restore();
94 throw;
95 }
96 restore();
97 });
98 }
99
100 RouteHistory(RouteHistory const&) = delete;
101 RouteHistory& operator=(RouteHistory const&) = delete;
102 RouteHistory(RouteHistory&&) = delete;
103 RouteHistory& operator=(RouteHistory&&) = delete;
104
105 // The history keeps its callback, which does nothing once this is gone: clearing it here could clear the
106 // callback of a newer route history over the same history.
107 ~RouteHistory() = default;
108
109 /// @brief The route the history held when this was made: a reloaded page's, or a shared link's.
110 /// @return It, or empty for none.
111 [[nodiscard]] std::string const& initial() const noexcept { return _initial; }
112
113 /// @brief The shell now shows a screen whose route is @p route.
114 /// @param route The route; empty, for a screen with none, leaves the history as it is, so after a move back or
115 /// forward the next screen with a route replaces the moved-to entry.
116 void shown(std::string const& route) {
117 if (route.empty()) {
118 return;
119 }
120 bool const replace = std::exchange(_replaceNext, false);
121 if (route == _history->current()) {
122 return;
123 }
124 if (replace) {
125 _history->replace(route);
126 } else {
127 _history->push(route);
128 }
129 }
130
131private:
132 IHistory* _history;
133 std::function<void(std::string const&)> _open;
134 std::string _initial;
135 bool _replaceNext = true;
136 // Expires with this, so a pop whose `open` destroyed it does not touch it again.
137 std::shared_ptr<bool const> _alive = std::make_shared<bool const>(true);
138};
139
140} // namespace morph::ui
A history of routes: a browser's session history, or a test's.
virtual void onPop(std::function< void(std::string const &route)> popped)=0
Sets what is called when the current entry changes from outside: back, forward, or an address the use...
virtual void push(std::string const &route)=0
Adds an entry after the current one, dropping any entries forward of it, and makes it current.
virtual void replace(std::string const &route)=0
Replaces the route the current entry holds.
virtual std::string current() const =0
The route the current entry holds.
Keeps the route of the screen a shell shows in a history, and opens the route of an entry the user mo...
void shown(std::string const &route)
The shell now shows a screen whose route is route.
std::string const & initial() const noexcept
The route the history held when this was made: a reloaded page's, or a shared link's.
RouteHistory(IHistory &history, std::function< void(std::string const &route)> open)
A history over history, which it keeps and follows from now on.
std::string fragmentOf(std::string_view route)
The URL fragment that holds a route.
std::string routeOfFragment(std::string_view fragment)
The route a URL fragment holds.