morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
shell_frame.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#include "../attributes.hpp"
11#include "backend.hpp"
12#include "navigator.hpp"
13
14/// @file
15/// @brief `morph::ui::ShellFrame`: the frame a client shows its screens in — a sidebar with the navigator, a top bar,
16/// and the page the workspace's tabs sit on. Built from backend widgets, so every frontend shows it.
17///
18/// Specified in `docs/spec/ui/shell_frame.md`.
19
20namespace morph::ui {
21
22/// @brief The frame's width for the sidebar, in units.
23inline constexpr int kShellSidebarWidth = 28;
24
25/// @brief What a `ShellFrame` shows and reports.
27 /// @brief The application's name, at the head of the sidebar.
28 std::string applicationName;
29 /// @brief The navigator's entries and handlers.
31 /// @brief Called when the person asks to sign out; empty hides the sign-out button.
32 std::function<void()> onSignOut;
33};
34
35/// @brief The frame around a client's screens.
36///
37/// A row filling the window: the sidebar (surface `sidebar`, `kShellSidebarWidth` units wide), holding the
38/// application's name and a `Navigator`, and the main column holding the top bar (surface `toolbar`: the shown
39/// screen's title, a slot for its active tab's head row, the principal and a sign-out button) above the page
40/// (surface `page`), where the client places the workspace. Signed out, the sidebar and the top bar are hidden and
41/// the page is all there is. Neither copyable nor movable; owned by the owner thread of its backend.
43public:
44 /// @brief The frame, appended to @p parent.
45 /// @param backend The backend its widgets are made with. Borrowed.
46 /// @param parent The container it is appended to; null makes it a backend root.
47 /// @param options What it shows and reports.
48 ShellFrame(IViewBackend& backend MORPH_LIFETIMEBOUND, ContainerWidget* parent, ShellFrameOptions options)
49 : _backend{backend},
50 _root{backend.createStack(parent, Axis::Horizontal)},
51 _sidebar{backend.createStack(_root.get(), Axis::Horizontal)},
52 _sideStart{backend.createSpacer(_sidebar.get())},
53 _side{backend.createStack(_sidebar.get(), Axis::Vertical)},
54 _sideTop{backend.createSpacer(_side.get())},
55 _name{backend.createText(_side.get())},
56 _navigator{std::make_unique<Navigator>(backend, _side.get(), std::move(options.navigator))},
57 _sideEnd{backend.createSpacer(_sidebar.get())},
58 _main{backend.createStack(_root.get(), Axis::Vertical)},
59 _topBar{backend.createStack(_main.get(), Axis::Horizontal)},
60 _topStart{backend.createSpacer(_topBar.get())},
61 _title{backend.createText(_topBar.get())},
62 _headSlot{backend.createStack(_topBar.get(), Axis::Horizontal)},
63 _principal{backend.createText(_topBar.get())},
64 _signOut{backend.createButton(_topBar.get())},
65 _topEnd{backend.createSpacer(_topBar.get())},
66 _page{backend.createStack(_main.get(), Axis::Vertical)},
67 _top{backend.createSpacer(_page.get())},
68 _padded{backend.createStack(_page.get(), Axis::Horizontal)},
69 _left{backend.createSpacer(_padded.get())},
70 _content{backend.createStack(_padded.get(), Axis::Vertical)},
71 _right{backend.createSpacer(_padded.get())} {
72 _root->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
73 _root->setTestId("shell");
74
75 _sidebar->setSurface("sidebar");
76 _sidebar->setLayout({.width = Sizing::fixed(kShellSidebarWidth), .height = Sizing::stretch()});
77 _sidebar->setGap(3);
78 _sidebar->setTestId("shell_sidebar");
79 // Spacers either side and above pad the sidebar's content by its gaps.
80 _sideStart->setLayout({.width = Sizing::fixed(0), .height = {}});
81 _side->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
82 _side->setGap(3);
83 _sideTop->setLayout({.width = {}, .height = Sizing::fixed(0)});
84 _sideEnd->setLayout({.width = Sizing::fixed(0), .height = {}});
85 _name->setRole(TextRole::Heading);
86 _name->setText(options.applicationName);
87 _name->setTestId("shell_application");
88
89 _main->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
90 _topBar->setSurface("toolbar");
91 _topBar->setGap(3);
92 // Spacers at both ends pad the bar by its gap.
93 _topStart->setLayout({.width = Sizing::fixed(0), .height = {}});
94 _topEnd->setLayout({.width = Sizing::fixed(0), .height = {}});
95 _topBar->setTestId("shell_topbar");
96 // Centred across the bar, so the principal's text sits level with the sign-out button's label.
97 _topBar->setAlign(Align::Center);
98 _title->setRole(TextRole::Heading);
99 _title->setLayout({.width = Sizing::stretch(), .height = {}});
100 _title->setTestId("shell_title");
101 // Its own stretch, independent of whether the row mounted inside it is currently visible: the mounted
102 // content's own visibility (hidden while a screen has no head) must not collapse the slot's width, since
103 // MorphStack.qml reads the *slot's* widthKind to decide whether the bar as a whole stretches or packs.
104 _headSlot->setLayout({.width = Sizing::stretch(), .height = {}});
105 _headSlot->setTestId("shell_head");
106 _principal->setRole(TextRole::Muted);
107 _principal->setTestId("shell_principal");
108 _signOut->setLabel("Sign out");
109 _signOut->setTestId("shell_signOut");
110 _signOut->setVisible(static_cast<bool>(options.onSignOut));
111 _signOut->setOnClick(std::move(options.onSignOut));
112
113 // The page is padded by spacing step 4: zero-sized spacers above and either side, each a gap away.
114 _page->setSurface("page");
115 _page->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
116 _page->setGap(4);
117 _page->setTestId("shell_page");
118 _top->setLayout({.width = {}, .height = Sizing::fixed(0)});
119 _padded->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
120 _padded->setGap(4);
121 _left->setLayout({.width = Sizing::fixed(0), .height = {}});
122 _content->setLayout({.width = Sizing::stretch(), .height = Sizing::stretch()});
123 _right->setLayout({.width = Sizing::fixed(0), .height = {}});
124 }
125
126 ShellFrame(ShellFrame const&) = delete;
127 ShellFrame& operator=(ShellFrame const&) = delete;
128 ShellFrame(ShellFrame&&) = delete;
129 ShellFrame& operator=(ShellFrame&&) = delete;
130 ~ShellFrame() = default;
131
132 /// @brief The container the client places the workspace in.
133 /// @return It.
134 [[nodiscard]] ContainerWidget& page() noexcept { return *_content; }
135
136 /// @brief The navigator in the sidebar.
137 /// @return It.
138 [[nodiscard]] Navigator& navigator() noexcept { return *_navigator; }
139
140 /// @brief The container in the top bar a client mounts the active screen's head row into, beside the title.
141 /// @return It.
142 [[nodiscard]] ContainerWidget& headSlot() noexcept { return *_headSlot; }
143
144 /// @brief Shows the shown screen's title in the top bar.
145 /// @param title The title; empty shows none.
146 void setTitle(std::string_view title) { _title->setText(title); }
147
148 /// @brief Shows who is signed in, in the top bar.
149 /// @param principal The principal's name; empty shows none.
150 void setPrincipal(std::string_view principal) { _principal->setText(principal); }
151
152 /// @brief Shows or hides the sidebar and the top bar: signed out, the page is all there is.
153 /// @param signedIn Whether someone is signed in.
154 void showSignedIn(bool signedIn) {
155 _sidebar->setVisible(signedIn);
156 _topBar->setVisible(signedIn);
157 }
158
159 /// @brief Shows a passive, transient message over the frame's main content area. Made the first time it is
160 /// asked for; a later call replaces its text and tone and restarts its dismiss interval, whether or not
161 /// it is still showing.
162 /// @param text The message, UTF-8.
163 /// @param tone The tone, mapped to the backend's theme.
164 void showToast(std::string_view text, Tone tone) {
165 if (_toast == nullptr) {
166 _toast = _backend.createToast(_main.get());
167 }
168 _toast->show(text, tone);
169 }
170
171private:
172 // Declared parent before child, so destruction takes the children first.
173 IViewBackend& _backend;
174 std::unique_ptr<StackWidget> _root;
175 std::unique_ptr<StackWidget> _sidebar;
176 std::unique_ptr<SpacerWidget> _sideStart;
177 std::unique_ptr<StackWidget> _side;
178 std::unique_ptr<SpacerWidget> _sideTop;
179 std::unique_ptr<TextWidget> _name;
180 std::unique_ptr<Navigator> _navigator;
181 std::unique_ptr<SpacerWidget> _sideEnd;
182 std::unique_ptr<StackWidget> _main;
183 // Lazily made, the first time showToast is called.
184 std::unique_ptr<ToastWidget> _toast;
185 std::unique_ptr<StackWidget> _topBar;
186 std::unique_ptr<SpacerWidget> _topStart;
187 std::unique_ptr<TextWidget> _title;
188 std::unique_ptr<StackWidget> _headSlot;
189 std::unique_ptr<TextWidget> _principal;
190 std::unique_ptr<ButtonWidget> _signOut;
191 std::unique_ptr<SpacerWidget> _topEnd;
192 std::unique_ptr<StackWidget> _page;
193 std::unique_ptr<SpacerWidget> _top;
194 std::unique_ptr<StackWidget> _padded;
195 std::unique_ptr<SpacerWidget> _left;
196 std::unique_ptr<StackWidget> _content;
197 std::unique_ptr<SpacerWidget> _right;
198};
199
200} // namespace morph::ui
A widget with children. A factory given this container appends the new widget as its last child.
Definition backend.hpp:103
Makes widgets. Each factory appends the new widget to parent, or makes a root when it is null,...
Definition backend.hpp:810
The catalog as a list a person opens screens from, with a title search and pinned shortcuts.
Definition navigator.hpp:57
void showToast(std::string_view text, Tone tone)
Shows a passive, transient message over the frame's main content area. Made the first time it is aske...
ContainerWidget & headSlot() noexcept
The container in the top bar a client mounts the active screen's head row into, beside the title.
ShellFrame(IViewBackend &backend, ContainerWidget *parent, ShellFrameOptions options)
The frame, appended to parent.
void setPrincipal(std::string_view principal)
Shows who is signed in, in the top bar.
ContainerWidget & page() noexcept
The container the client places the workspace in.
void setTitle(std::string_view title)
Shows the shown screen's title in the top bar.
Navigator & navigator() noexcept
The navigator in the sidebar.
void showSignedIn(bool signedIn)
Shows or hides the sidebar and the top bar: signed out, the page is all there is.
The workspace's navigator and pins (spec 6 §7): the catalog listed by module and group,...
constexpr int kShellSidebarWidth
The frame's width for the sidebar, in units.
How a Navigator lists the catalog and what it reports.
Definition navigator.hpp:28
What a ShellFrame shows and reports.
NavigatorOptions navigator
The navigator's entries and handlers.
std::string applicationName
The application's name, at the head of the sidebar.
std::function< void()> onSignOut
Called when the person asks to sign out; empty hides the sign-out button.
static constexpr Sizing stretch(int weight=1) noexcept
A share of the leftover space.
Definition view.hpp:294
static constexpr Sizing fixed(int units) noexcept
A fixed size.
Definition view.hpp:289
The backend contract: the retained widgets a mount drives, and the factory that makes them.
Tone
What a message or a mark conveys, mapped to the theme's colours.
Definition view.hpp:240
@ Heading
A section heading.
Definition view.hpp:202
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194
Axis
The direction a stack arranges, or a scroll area scrolls, its content.
Definition view.hpp:222
@ Vertical
Top to bottom.
Definition view.hpp:223
@ Horizontal
Left to right.
Definition view.hpp:224