Rewild Your Web
web
browser
dweb
1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3import { EdgeGestureHandler } from "./edge_gesture_handler.js";
4
5export class MobileLayoutManager {
6 constructor(rootElement, webViewBuilder) {
7 this.root = rootElement;
8 this.webViewBuilder = webViewBuilder;
9
10 // Linear array of webviews (carousel model)
11 this.webviews = new Map(); // webviewId -> { webview, index }
12 this.webviewOrder = []; // Array of webviewIds in order
13 this.activeIndex = -1;
14 this.activeWebviewId = null;
15 this.nextId = 1;
16
17 // Homescreen webview (special, cannot be closed, excluded from overview/swipe)
18 this.homescreenWebviewId = null;
19
20 // Overview mode state
21 this.overviewMode = false;
22 this.overviewElement = null;
23
24 // Gesture handler
25 this.gestureHandler = new EdgeGestureHandler(this);
26
27 // Action bar component
28 this.actionBar = null;
29
30 // Setup event listeners
31 this.setupEventListeners();
32 }
33
34 setupEventListeners() {
35 document.addEventListener("webview-close", (e) => {
36 this.removeWebView(e.detail.webviewId);
37 });
38
39 document.addEventListener("webview-focus", (e) => {
40 this.setActiveWebView(e.detail.webviewId);
41 });
42
43 // Listen for navigation state changes from webviews
44 document.addEventListener("navigation-state-changed", (e) => {
45 // Only update if it's from the active webview
46 if (e.detail.webviewId === this.activeWebviewId && this.actionBar) {
47 this.actionBar.updateState();
48 }
49 });
50 }
51
52 generateId() {
53 return `wv-${this.nextId++}`;
54 }
55
56 // Set the homescreen webview
57 setHomescreen(webviewId) {
58 this.homescreenWebviewId = webviewId;
59 }
60
61 // Check if a webview is the homescreen
62 isHomescreen(webviewId) {
63 return webviewId === this.homescreenWebviewId;
64 }
65
66 // Get the currently active webview entry
67 getActiveEntry() {
68 if (this.activeWebviewId) {
69 return this.webviews.get(this.activeWebviewId);
70 }
71 return null;
72 }
73
74 // Add a new webview to the carousel
75 addWebView(webview) {
76 const id = this.generateId();
77 webview.webviewId = id;
78
79 // Mark webview as mobile mode for styling
80 webview.classList.add("mobile-mode");
81
82 // Add to the end of the order
83 const index = this.webviewOrder.length;
84 this.webviewOrder.push(id);
85 this.webviews.set(id, { webview, index });
86
87 // Create container for the webview
88 const container = document.createElement("div");
89 container.className = "mobile-webview-container";
90 container.dataset.webviewId = id;
91 container.appendChild(webview);
92 this.root.appendChild(container);
93
94 // Make this the active webview
95 this.setActiveWebView(id);
96
97 return webview;
98 }
99
100 // Remove a webview from the carousel
101 removeWebView(webviewId) {
102 // Prevent closing the homescreen
103 if (this.isHomescreen(webviewId)) {
104 console.warn("[MobileLayoutManager] Cannot close homescreen");
105 return;
106 }
107
108 const entry = this.webviews.get(webviewId);
109 if (!entry) {
110 return;
111 }
112
113 const { webview, index } = entry;
114
115 // Remove from DOM
116 const container = this.root.querySelector(
117 `.mobile-webview-container[data-webview-id="${webviewId}"]`,
118 );
119 if (container) {
120 container.remove();
121 }
122
123 // Remove from tracking
124 this.webviews.delete(webviewId);
125 this.webviewOrder = this.webviewOrder.filter((id) => id !== webviewId);
126
127 // Update indices for remaining webviews
128 this.webviewOrder.forEach((id, newIndex) => {
129 const e = this.webviews.get(id);
130 if (e) {
131 e.index = newIndex;
132 }
133 });
134
135 // If we removed the active webview, activate another one
136 if (this.activeWebviewId === webviewId) {
137 if (this.webviewOrder.length > 0) {
138 // Activate the webview at the same position or the last one
139 const newIndex = Math.min(index, this.webviewOrder.length - 1);
140 this.setActiveWebView(this.webviewOrder[newIndex]);
141 } else {
142 this.activeWebviewId = null;
143 this.activeIndex = -1;
144 }
145 }
146 }
147
148 // Set the active webview by ID
149 setActiveWebView(webviewId) {
150 // Remove active state from previous and capture its screenshot
151 if (this.activeWebviewId && this.webviews.has(this.activeWebviewId)) {
152 const prevEntry = this.webviews.get(this.activeWebviewId);
153 prevEntry.webview.active = false;
154
155 // Capture screenshot of the tab we're switching away from
156 // TODO: fix screenshot capture when not fully visible.
157 // prevEntry.webview.captureScreenshot(true);
158
159 const prevContainer = this.root.querySelector(
160 `.mobile-webview-container[data-webview-id="${this.activeWebviewId}"]`,
161 );
162 if (prevContainer) {
163 prevContainer.classList.remove("active");
164 }
165 }
166
167 this.activeWebviewId = webviewId;
168
169 // Set active state on new
170 if (webviewId && this.webviews.has(webviewId)) {
171 const entry = this.webviews.get(webviewId);
172 entry.webview.active = true;
173 this.activeIndex = entry.index;
174 const container = this.root.querySelector(
175 `.mobile-webview-container[data-webview-id="${webviewId}"]`,
176 );
177 if (container) {
178 container.classList.add("active");
179 }
180 }
181 }
182
183 // Switch to webview at given index
184 switchTo(index) {
185 if (index < 0 || index >= this.webviewOrder.length) {
186 return;
187 }
188 const webviewId = this.webviewOrder[index];
189 this.setActiveWebView(webviewId);
190 }
191
192 // Navigate to next webview in carousel (skips homescreen)
193 nextWebView() {
194 const regularViews = this.webviewOrder.filter(
195 (id) => !this.isHomescreen(id),
196 );
197 if (regularViews.length <= 1) {
198 return;
199 }
200
201 const currentIndex = regularViews.indexOf(this.activeWebviewId);
202 if (currentIndex === -1) {
203 // Currently on homescreen, switch to first regular view
204 this.setActiveWebView(regularViews[0]);
205 } else {
206 const nextIndex = (currentIndex + 1) % regularViews.length;
207 this.setActiveWebView(regularViews[nextIndex]);
208 }
209 }
210
211 // Navigate to previous webview in carousel (skips homescreen)
212 prevWebView() {
213 const regularViews = this.webviewOrder.filter(
214 (id) => !this.isHomescreen(id),
215 );
216 if (regularViews.length <= 1) {
217 return;
218 }
219
220 const currentIndex = regularViews.indexOf(this.activeWebviewId);
221 if (currentIndex === -1) {
222 // Currently on homescreen, switch to last regular view
223 this.setActiveWebView(regularViews[regularViews.length - 1]);
224 } else {
225 const prevIndex =
226 (currentIndex - 1 + regularViews.length) % regularViews.length;
227 this.setActiveWebView(regularViews[prevIndex]);
228 }
229 }
230
231 // Get adjacent webview ID for peek preview (skips homescreen)
232 getAdjacentWebViewId(direction) {
233 const regularViews = this.webviewOrder.filter(
234 (id) => !this.isHomescreen(id),
235 );
236 if (regularViews.length <= 1) {
237 return null;
238 }
239
240 const currentIndex = regularViews.indexOf(this.activeWebviewId);
241 if (currentIndex === -1) {
242 // On homescreen, no peek
243 return null;
244 }
245
246 if (direction === "next") {
247 const nextIndex = (currentIndex + 1) % regularViews.length;
248 return regularViews[nextIndex];
249 } else {
250 const prevIndex =
251 (currentIndex - 1 + regularViews.length) % regularViews.length;
252 return regularViews[prevIndex];
253 }
254 }
255
256 // Get webview info for peek preview
257 getWebViewInfo(webviewId) {
258 const entry = this.webviews.get(webviewId);
259 if (!entry) {
260 return null;
261 }
262 return {
263 id: webviewId,
264 title: entry.webview.title || "Untitled",
265 favicon: entry.webview.favicon || "",
266 screenshotUrl: entry.webview.screenshotUrl || null,
267 };
268 }
269
270 // Show action bar (bottom slide-up)
271 // Not allowed when on homescreen
272 showActionBar() {
273 if (this.isHomescreen(this.activeWebviewId)) {
274 return;
275 }
276 if (this.actionBar) {
277 this.actionBar.show();
278 }
279 }
280
281 // Hide action bar
282 hideActionBar() {
283 if (this.actionBar) {
284 this.actionBar.hide();
285 }
286 }
287
288 // Toggle action bar
289 toggleActionBar() {
290 if (this.actionBar) {
291 this.actionBar.toggle();
292 }
293 }
294
295 // Set the action bar component reference
296 setActionBar(actionBar) {
297 this.actionBar = actionBar;
298 }
299
300 // Perform navigation action on active webview
301 goBack() {
302 const entry = this.getActiveEntry();
303 if (entry) {
304 entry.webview.goBack();
305 }
306 }
307
308 goForward() {
309 const entry = this.getActiveEntry();
310 if (entry) {
311 entry.webview.goForward();
312 }
313 }
314
315 reload() {
316 const entry = this.getActiveEntry();
317 if (entry) {
318 entry.webview.doReload();
319 }
320 }
321
322 // Navigate to URL in active webview
323 navigateTo(url) {
324 const entry = this.getActiveEntry();
325 if (entry) {
326 entry.webview.ensureIframe();
327 entry.webview.iframe.load(url);
328 }
329 }
330
331 // Get current URL of active webview
332 getCurrentUrl() {
333 const entry = this.getActiveEntry();
334 if (entry) {
335 return entry.webview.currentUrl || "";
336 }
337 return "";
338 }
339
340 // Get navigation state of active webview
341 getNavigationState() {
342 const entry = this.getActiveEntry();
343 if (entry) {
344 return {
345 canGoBack: entry.webview.canGoBack || false,
346 canGoForward: entry.webview.canGoForward || false,
347 };
348 }
349 return { canGoBack: false, canGoForward: false };
350 }
351
352 // Get tab count (excludes homescreen)
353 getTabCount() {
354 return this.webviewOrder.filter((id) => !this.isHomescreen(id)).length;
355 }
356
357 // Get tabs for overview (excludes homescreen)
358 getOverviewTabs() {
359 return this.webviewOrder
360 .filter((id) => !this.isHomescreen(id))
361 .map((id) => {
362 const entry = this.webviews.get(id);
363 return {
364 id,
365 title: entry.webview.title || "Untitled",
366 favicon: entry.webview.favicon || "",
367 screenshotUrl: entry.webview.screenshotUrl || null,
368 };
369 });
370 }
371
372 // ============================================================================
373 // Overview Mode (for compatibility, minimal implementation for MVP)
374 // ============================================================================
375
376 toggleOverview() {
377 if (this.overviewMode) {
378 this.hideOverview();
379 } else {
380 this.showOverview();
381 }
382 }
383
384 showOverview() {
385 this.overviewMode = true;
386 // TODO: Implement mobile tab overview grid
387 console.log(
388 "[MobileLayoutManager] Overview mode enabled (not yet implemented)",
389 );
390 }
391
392 hideOverview() {
393 this.overviewMode = false;
394 console.log("[MobileLayoutManager] Overview mode disabled");
395 }
396
397 // Navigation methods for compatibility with desktop shortcuts
398 nextPanel() {
399 this.nextWebView();
400 }
401
402 prevPanel() {
403 this.prevWebView();
404 }
405
406 scrollToPanel(index) {
407 this.switchTo(index);
408 }
409}