Skip to content
223 linesCodeBlameRaw
1// The title bar's work: draw what the page under it reports, and send its
2// buttons to the app (src/bridge.rs: title_bar_current, title_bar_act,
3// window_control, window_menu). A click here hands the keyboard back to
4// the page, so typing goes on where it was.
5(function () {
6 var core = window.__TAURI__ && window.__TAURI__.core;
7 var events = window.__TAURI__ && window.__TAURI__.event;
8 function invoke(command, args) {
9 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
10 return Promise.reject(new Error("the title bar cannot reach the app"));
11 }
12 function quietly(promise) {
13 promise.catch(function () {});
14 }
15 function act(action) {
16 quietly(invoke("title_bar_act", { action: action }));
17 }
18 function control(action) {
19 return invoke("window_control", { action: action });
20 }
21
22 var ua = navigator.userAgent;
23 var MAC = /Macintosh|Mac OS X/.test(ua) && !/Windows/.test(ua);
24 if (MAC) document.documentElement.classList.add("mac");
25
26 function svg(body) {
27 return (
28 '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
29 body +
30 "</svg>"
31 );
32 }
33 // Drawn here, from lines and boxes, as each system draws its own.
34 var ICONS = {
35 menu: svg('<line x1="3" y1="4.5" x2="13" y2="4.5"/><line x1="3" y1="8" x2="13" y2="8"/><line x1="3" y1="11.5" x2="13" y2="11.5"/>'),
36 back: svg('<polyline points="9.5,3.5 5,8 9.5,12.5"/><line x1="5" y1="8" x2="13" y2="8"/>'),
37 forward: svg('<polyline points="6.5,3.5 11,8 6.5,12.5"/><line x1="3" y1="8" x2="11" y2="8"/>'),
38 search: svg('<circle cx="7" cy="7" r="4"/><line x1="10" y1="10" x2="13" y2="13"/>'),
39 settings: svg('<line x1="2.5" y1="4.5" x2="13.5" y2="4.5"/><line x1="2.5" y1="11.5" x2="13.5" y2="11.5"/><circle cx="10" cy="4.5" r="1.75" fill="var(--bg)"/><circle cx="6" cy="11.5" r="1.75" fill="var(--bg)"/>'),
40 minimize: svg('<line x1="3.5" y1="8" x2="12.5" y2="8"/>'),
41 maximize: svg('<rect x="3.5" y="3.5" width="9" height="9" rx="1"/>'),
42 restore: svg('<rect x="3.5" y="5.5" width="7" height="7" rx="1"/><path d="M5.5 5.5V4.5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1h-1"/>'),
43 close: svg('<line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/>'),
44 };
45
46 function button(kind, label, icon, onClick) {
47 var b = document.createElement("button");
48 b.type = "button";
49 b.tabIndex = -1;
50 b.className = kind;
51 b.setAttribute("aria-label", label);
52 b.title = label;
53 if (icon) b.innerHTML = ICONS[icon];
54 b.addEventListener("click", function (event) {
55 event.stopPropagation();
56 onClick(b);
57 });
58 return b;
59 }
60
61 var bar = document.getElementById("bar");
62 var left = document.getElementById("left");
63 var middle = document.getElementById("middle");
64 var right = document.getElementById("right");
65
66 if (!MAC) {
67 left.append(
68 button("icon", "Menu", "menu", function (b) {
69 var box = b.getBoundingClientRect();
70 quietly(invoke("window_menu", { x: box.left, y: box.bottom }));
71 }),
72 );
73 }
74 var back = button("icon", "Back", "back", function () {
75 act("back");
76 });
77 var forward = button("icon", "Forward", "forward", function () {
78 act("forward");
79 });
80 left.append(back, forward);
81
82 // The runner, as a dot right beside the settings: its colour says what
83 // it is doing, live, and a wide window says it in words too. A click
84 // opens its menu, which the site draws under the dot (or, with no page
85 // of the site's to draw it, the Runner window). The words are the app's
86 // (diagnosis.rs chip).
87 var chip = button("chip", "Runner", null, function (b) {
88 var box = b.getBoundingClientRect();
89 var width = document.documentElement.clientWidth || window.innerWidth || 1;
90 // Where the dot's right edge is, as a share of the bar, so the menu
91 // lines up under it whatever the site's zoom.
92 quietly(invoke("title_bar_act", { action: "runner", at: box.right / width }));
93 });
94 chip.hidden = true;
95 chip.setAttribute("aria-haspopup", "dialog");
96 chip.setAttribute("aria-expanded", "false");
97 var chipDot = document.createElement("span");
98 chipDot.className = "chip-dot";
99 var chipText = document.createElement("span");
100 chipText.className = "chip-text";
101 chip.append(chipDot, chipText);
102 function drawChip(said) {
103 if (!said || !said.label) return;
104 chip.hidden = false;
105 chip.dataset.tone = said.tone;
106 chipText.textContent = said.label;
107 chip.setAttribute("aria-label", /runner/i.test(said.label) ? said.label : "Runner: " + said.label);
108 chip.title = said.label;
109 }
110 // The app's settings, at the right of the bar, before the window's
111 // buttons: where they are found without the menu.
112 right.append(
113 chip,
114 button("icon", "Settings", "settings", function () {
115 act("settings");
116 }),
117 );
118
119 var maximize = null;
120 function drawMaximized(maximized) {
121 if (!maximize) return;
122 maximize.innerHTML = maximized ? ICONS.restore : ICONS.maximize;
123 maximize.setAttribute("aria-label", maximized ? "Restore" : "Maximize");
124 maximize.title = maximized ? "Restore" : "Maximize";
125 }
126 // The window's buttons, at the right as Windows and Linux put them; macOS has its own.
127 if (!MAC) {
128 maximize = button("caption", "Maximize", "maximize", function () {
129 control("maximize").then(drawMaximized, function () {});
130 });
131 right.append(
132 button("caption", "Minimize", "minimize", function () {
133 quietly(control("minimize"));
134 }),
135 maximize,
136 button("caption close", "Close", "close", function () {
137 quietly(control("close"));
138 }),
139 );
140 var resizing = 0;
141 window.addEventListener("resize", function () {
142 clearTimeout(resizing);
143 resizing = setTimeout(function () {
144 control("maximized").then(drawMaximized, function () {});
145 }, 120);
146 });
147 control("maximized").then(drawMaximized, function () {});
148 }
149
150 // Dragging the bar moves the window; a double click maximizes it, as a system's title bar does.
151 bar.addEventListener("mousedown", function (event) {
152 if (event.button !== 0 || event.target.closest("button")) return;
153 if (event.detail === 2) {
154 control("maximize").then(drawMaximized, function () {});
155 return;
156 }
157 quietly(control("drag"));
158 });
159 // The keyboard belongs to the page: a click here gives it back.
160 window.addEventListener("mouseup", function () {
161 act("focus");
162 });
163
164 var title = document.createElement("span");
165 title.className = "title";
166 var pill = button("search", "Search", "search", function () {
167 act("search");
168 });
169 var pillText = document.createElement("span");
170 pill.append(pillText);
171
172 function setColors(colors) {
173 var root = document.documentElement.style;
174 if (!colors) return;
175 var shade = colors.dark ? "237, 237, 239" : "15, 15, 17";
176 root.setProperty("--bg", colors.background);
177 root.setProperty("--fg", colors.foreground);
178 root.setProperty("--muted", colors.muted);
179 root.setProperty("--accent", colors.accent);
180 root.setProperty("--hover", "rgba(" + shade + ", " + (colors.dark ? 0.1 : 0.07) + ")");
181 root.setProperty("--pill", "rgba(" + shade + ", " + (colors.dark ? 0.09 : 0.05) + ")");
182 root.setProperty("--pill-hover", "rgba(" + shade + ", " + (colors.dark ? 0.14 : 0.09) + ")");
183 root.setProperty("--pill-edge", "rgba(" + shade + ", " + (colors.dark ? 0.08 : 0.1) + ")");
184 document.documentElement.style.colorScheme = colors.dark ? "dark" : "light";
185 }
186
187 function draw(page) {
188 if (!page) return;
189 middle.textContent = "";
190 if (page.search) {
191 pillText.textContent = page.search;
192 pill.setAttribute("aria-label", page.search);
193 pill.removeAttribute("title");
194 middle.append(pill);
195 } else {
196 title.textContent = page.title || "g1t";
197 middle.append(title);
198 }
199 chip.setAttribute("aria-expanded", page.runnerMenu ? "true" : "false");
200 back.disabled = page.canGoBack === false;
201 forward.disabled = page.canGoForward === false;
202 setColors(page.colors);
203 }
204
205 if (events && typeof events.listen === "function") {
206 events
207 .listen("g1t-title-bar", function (event) {
208 draw(event.payload);
209 })
210 .catch(function () {});
211 }
212 invoke("title_bar_current").then(draw, function () {});
213 if (events && typeof events.listen === "function") {
214 events
215 .listen("g1t-runner-status", function (event) {
216 drawChip(event.payload);
217 })
218 .catch(function () {});
219 }
220 invoke("runner_status").then(function (view) {
221 drawChip(view && view.chip);
222 }, function () {});
223})();