Rewild Your Web

mobile: simplify mobile specific markup and add a splash screen

+320 -38
+1 -1
htmlshell/Cargo.toml
··· 6 6 edition.workspace = true 7 7 8 8 [features] 9 - default = ["libservo/clipboard", "js_jit", "max_log_level", "media-gstreamer", "native-bluetooth", "webgpu"] 9 + default = ["libservo/clipboard", "js_jit", "max_log_level", "native-bluetooth", "webgpu"] 10 10 gamepad = ["libservo/gamepad"] 11 11 global-hotkeys = ["dep:global-hotkey"] 12 12 crown = ["libservo/crown"]
+31 -6
htmlshell/src/main.rs
··· 645 645 646 646 impl ServoDelegate for ShutdownDelegate {} 647 647 648 + // Computes the index url depending on preferences and screen size. 649 + fn index_url(event_loop: &ActiveEventLoop) -> String { 650 + let pref_override = match get_embedder_pref("browserhtml.mobile_simulation") { 651 + Some(PrefValue::Bool(value)) => value, 652 + _ => false, 653 + }; 654 + 655 + let primary_monitor = event_loop 656 + .primary_monitor() 657 + .or_else(|| event_loop.available_monitors().next()); 658 + 659 + let mobile_screen = match primary_monitor { 660 + Some(monitor) => { 661 + // TODO: better way to specify the device type. 662 + monitor.size().width < monitor.size().height 663 + }, 664 + _ => false, 665 + }; 666 + 667 + if pref_override || mobile_screen { 668 + "http://system.localhost:8888/index_mobile.html".into() 669 + } else { 670 + "http://system.localhost:8888/index.html".into() 671 + } 672 + } 673 + 648 674 enum App { 649 675 Initial(Waker), 650 676 Running(Rc<AppState>), ··· 713 739 servo.setup_logging(); 714 740 enable_experimental_prefs(&servo); 715 741 716 - let url = Url::parse( 717 - &std::env::args() 718 - .nth(1) 719 - .unwrap_or("http://system.localhost:8888/index.html".to_owned()), 720 - ) 721 - .expect("Invalid url"); 742 + let start_url = index_url(event_loop); 743 + println!("Starting url: {start_url}"); 744 + 745 + let url = 746 + Url::parse(&std::env::args().nth(1).unwrap_or(start_url)).expect("Invalid url"); 722 747 723 748 // Create the initial window 724 749 app_state.open_window(event_loop, url, None);
+47 -3
ui/homescreen/index.js
··· 195 195 } 196 196 }); 197 197 198 - // Initialize 199 - loadWidgets(); 200 - loadBookmarks(); 198 + // Port for communicating with system app splash screen 199 + let splashPort = null; 200 + let homescreenReady = false; 201 + 202 + // Listen for splash port from system app 203 + window.addEventListener("message", (event) => { 204 + console.log("[Homescreen] Received message:", event.data, "ports:", event.ports); 205 + if (event.data?.type === "splash-port" && event.ports?.length > 0) { 206 + console.log("[Homescreen] Splash port received"); 207 + splashPort = event.ports[0]; 208 + // If homescreen is already ready, send the signal now 209 + if (homescreenReady) { 210 + sendReadySignal(); 211 + } 212 + } 213 + }); 214 + 215 + // Actually send the ready signal 216 + function sendReadySignal() { 217 + if (splashPort) { 218 + try { 219 + splashPort.postMessage({ type: "homescreen-ready" }); 220 + console.log("[Homescreen] Ready signal sent successfully"); 221 + } catch (e) { 222 + console.error("[Homescreen] Failed to send ready signal:", e); 223 + } 224 + } 225 + } 226 + 227 + // Signal ready to system app 228 + function signalReady() { 229 + console.log("[Homescreen] signalReady called, splashPort:", splashPort); 230 + homescreenReady = true; 231 + if (splashPort) { 232 + sendReadySignal(); 233 + } else { 234 + console.log("[Homescreen] Port not yet available, will send when received"); 235 + } 236 + } 237 + 238 + // Initialize and signal when ready 239 + async function initialize() { 240 + await Promise.all([loadWidgets(), loadBookmarks()]); 241 + signalReady(); 242 + } 243 + 244 + initialize();
+1 -2
ui/system/index.html
··· 9 9 <link rel="stylesheet" href="//shared.localhost:8888/third_party/lucide/lucide.css" /> 10 10 <script type="module" src="//shared.localhost:8888/lucide_icon.js"></script> 11 11 <link rel="stylesheet" href="index.css" /> 12 - <link rel="stylesheet" href="mobile.css" /> 13 12 <script src="./third_party/mousetrap/mousetrap.min.js"></script> 14 13 <script src="./third_party/mousetrap/mousetrap-global-bind.min.js"></script> 15 14 <script src="./third_party/fend/fend_wasm.js"></script> ··· 20 19 <body> 21 20 <header id="header"> 22 21 <lucide-icon class="resize" name="move-diagonal-2"></lucide-icon> 23 - <span>Browser.HTML</span> 22 + <span>Beaver</span> 24 23 <lucide-icon class="close" name="x"></lucide-icon> 25 24 </header> 26 25 <main>
+149 -26
ui/system/index.js
··· 404 404 return false; 405 405 }); 406 406 407 + // ============================================================================ 408 + // Mobile Splash Screen 409 + // ============================================================================ 410 + 411 + class MobileSplashScreen { 412 + static TIMEOUT_MS = 5000; 413 + static FADE_DURATION_MS = 400; 414 + 415 + constructor() { 416 + this.element = document.getElementById("mobile-splash"); 417 + this.timeoutId = null; 418 + this.channel = null; 419 + } 420 + 421 + show() { 422 + if (this.element) { 423 + this.element.classList.add("active"); 424 + } 425 + } 426 + 427 + hide() { 428 + if (!this.element || !this.element.classList.contains("active")) { 429 + return; 430 + } 431 + 432 + if (this.timeoutId) { 433 + clearTimeout(this.timeoutId); 434 + this.timeoutId = null; 435 + } 436 + 437 + this.element.classList.add("fade-out"); 438 + setTimeout(() => { 439 + this.element.classList.remove("active", "fade-out"); 440 + }, MobileSplashScreen.FADE_DURATION_MS); 441 + } 442 + 443 + /** 444 + * Wait for the homescreen webview to be ready. 445 + * Sends a MessagePort to the homescreen and waits for "ready" message. 446 + * Falls back to timeout if no response received. 447 + */ 448 + async waitForHomescreen(homescreenWebView) { 449 + // Set up timeout fallback 450 + const timeoutPromise = new Promise((resolve) => { 451 + this.timeoutId = setTimeout(() => { 452 + console.warn("[Splash] Timeout reached, hiding splash screen"); 453 + resolve(); 454 + }, MobileSplashScreen.TIMEOUT_MS); 455 + }); 456 + 457 + // Set up MessageChannel for homescreen communication 458 + const readyPromise = new Promise(async (resolve) => { 459 + // Wait for WebView to render so iframe exists in shadow DOM 460 + await homescreenWebView.updateComplete; 461 + 462 + const iframe = homescreenWebView.shadowRoot?.querySelector("iframe"); 463 + if (!iframe) { 464 + console.error("[Splash] No iframe found in WebView shadow DOM"); 465 + return; 466 + } 467 + 468 + let channelSetup = false; 469 + 470 + // Listen for embedloadstatuschange on the iframe 471 + const onLoadStatus = (event) => { 472 + console.log("[Splash] embedloadstatuschange event:", event.detail); 473 + if (channelSetup) { 474 + return; 475 + } 476 + 477 + // Wait for "complete" status like the keyboard does 478 + if (event.detail === "complete") { 479 + channelSetup = true; 480 + iframe.removeEventListener("embedloadstatuschange", onLoadStatus); 481 + this.setupChannel(iframe, resolve); 482 + } 483 + }; 484 + 485 + iframe.addEventListener("embedloadstatuschange", onLoadStatus); 486 + }); 487 + 488 + // Race: either homescreen signals ready or timeout fires 489 + await Promise.race([readyPromise, timeoutPromise]); 490 + this.hide(); 491 + } 492 + 493 + setupChannel(iframe, onReady) { 494 + this.channel = new MessageChannel(); 495 + 496 + this.channel.port1.onmessage = (event) => { 497 + console.log("[Splash] Received message on port1:", event.data); 498 + if (event.data?.type === "homescreen-ready") { 499 + console.log("[Splash] Homescreen ready signal received"); 500 + onReady(); 501 + } 502 + }; 503 + 504 + this.channel.port1.onmessageerror = (event) => { 505 + console.error("[Splash] Message error on port1:", event); 506 + }; 507 + 508 + // Send port2 to homescreen 509 + console.log("[Splash] Sending port to homescreen iframe"); 510 + try { 511 + iframe.contentWindow.postMessage({ type: "splash-port" }, "*", [ 512 + this.channel.port2, 513 + ]); 514 + console.log("[Splash] postMessage sent successfully"); 515 + } catch (e) { 516 + console.error("[Splash] Failed to send postMessage:", e); 517 + } 518 + } 519 + } 520 + 407 521 async function openVirtualKeyboard(detail) { 408 522 // Check if virtual keyboard is enabled. 409 523 if ( ··· 482 596 483 597 // Setup mobile action bar if in mobile mode 484 598 if (isMobileMode) { 485 - // Create homescreen webview first 599 + // Create and show splash screen immediately 600 + const splash = new MobileSplashScreen(); 601 + splash.show(); 602 + 603 + // Create homescreen webview 486 604 const homescreenUrl = navigator.servo.getStringPreference( 487 605 "browserhtml.homescreen_url", 488 606 ); 489 607 const homescreen = new WebView(homescreenUrl, "Home", {}); 490 608 layoutManager.addWebView(homescreen); 491 609 layoutManager.setHomescreen(homescreen.webviewId); 610 + 611 + // Wait for homescreen to be ready (async, doesn't block other init) 612 + splash.waitForHomescreen(homescreen); 492 613 493 614 // Create mobile action bar 494 615 mobileActionBar = document.createElement("mobile-action-bar"); ··· 698 819 } 699 820 }); 700 821 701 - notificationPanel.addEventListener("notification-click", (e) => { 702 - const notification = e.detail.notification; 822 + if (notificationPanel) { 823 + notificationPanel.addEventListener("notification-click", (e) => { 824 + const notification = e.detail.notification; 703 825 704 - // Focus the source webview if possible 705 - if (notification.webviewId && layoutManager.webviews) { 706 - // Try to find the webview by its ID 707 - for (const [id, entry] of layoutManager.webviews) { 708 - if (id.toString() === notification.webviewId) { 709 - layoutManager.setActiveWebView(id); 710 - layoutManager.scrollToPanel(entry.panelIndex); 711 - break; 826 + // Focus the source webview if possible 827 + if (notification.webviewId && layoutManager.webviews) { 828 + // Try to find the webview by its ID 829 + for (const [id, entry] of layoutManager.webviews) { 830 + if (id.toString() === notification.webviewId) { 831 + layoutManager.setActiveWebView(id); 832 + layoutManager.scrollToPanel(entry.panelIndex); 833 + break; 834 + } 712 835 } 713 836 } 714 - } 715 837 716 - // Dismiss the notification (no actions supported for now) 717 - dismissNotification(notification); 838 + // Dismiss the notification (no actions supported for now) 839 + dismissNotification(notification); 718 840 719 - // Close the panel 720 - notificationPanel.open = false; 721 - }); 841 + // Close the panel 842 + notificationPanel.open = false; 843 + }); 722 844 723 - notificationPanel.addEventListener("notification-dismiss", (e) => { 724 - dismissNotification(e.detail.notification); 725 - }); 845 + notificationPanel.addEventListener("notification-dismiss", (e) => { 846 + dismissNotification(e.detail.notification); 847 + }); 726 848 727 - notificationPanel.addEventListener("notification-clear-all", () => { 728 - clearAllNotifications(); 729 - }); 849 + notificationPanel.addEventListener("notification-clear-all", () => { 850 + clearAllNotifications(); 851 + }); 730 852 731 - notificationPanel.addEventListener("panel-closed", () => { 732 - notificationPanel.open = false; 733 - }); 853 + notificationPanel.addEventListener("panel-closed", () => { 854 + notificationPanel.open = false; 855 + }); 856 + } 734 857 735 858 // Listen for notifications from webviews 736 859 document
+36
ui/system/index_mobile.html
··· 1 + <!DOCTYPE html> 2 + <!-- SPDX-License-Identifier: AGPL-3.0-or-later --> 3 + <html> 4 + <head> 5 + <title>Browser.HTML</title> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" /> 7 + <link rel="stylesheet" href="//theme.localhost:8888/index.css" /> 8 + <link rel="stylesheet" href="//shared.localhost:8888/fonts/fonts.css" /> 9 + <link rel="stylesheet" href="//shared.localhost:8888/third_party/lucide/lucide.css" /> 10 + <script type="module" src="//shared.localhost:8888/lucide_icon.js"></script> 11 + <link rel="stylesheet" href="index.css" /> 12 + <link rel="stylesheet" href="mobile.css" /> 13 + <script src="./third_party/mousetrap/mousetrap.min.js"></script> 14 + <script src="./third_party/mousetrap/mousetrap-global-bind.min.js"></script> 15 + <script src="./third_party/fend/fend_wasm.js"></script> 16 + <script type="module" src="//shared.localhost:8888/theme.js"></script> 17 + <script type="module" src="notification_panel.js"></script> 18 + <script type="module" src="index.js"></script> 19 + </head> 20 + <body> 21 + <div id="mobile-splash" class="mobile-splash"> 22 + <div class="splash-content"> 23 + <div class="splash-logo">Beaver</div> 24 + <img src="./logo.png" alt="Beaver Logo"/> 25 + <div class="splash-spinner"> 26 + <lucide-icon name="loader-circle"></lucide-icon> 27 + </div> 28 + </div> 29 + </div> 30 + <main> 31 + <div id="root"></div> 32 + </main> 33 + <footer id="footer"> 34 + </footer> 35 + </body> 36 + </html>
+55
ui/system/mobile.css
··· 309 309 width: 100% !important; 310 310 height: 100% !important; 311 311 } 312 + 313 + /* ============================================================================ 314 + Mobile Splash Screen 315 + ============================================================================ */ 316 + 317 + .mobile-splash { 318 + position: fixed; 319 + top: 0; 320 + left: 0; 321 + right: 0; 322 + bottom: 0; 323 + z-index: var(--z-modal); 324 + background: black; 325 + display: none; 326 + align-items: center; 327 + justify-content: center; 328 + opacity: 1; 329 + transition: opacity 0.4s ease-out; 330 + } 331 + 332 + body.mobile-mode .mobile-splash.active { 333 + display: flex; 334 + } 335 + 336 + body.mobile-mode .mobile-splash.fade-out { 337 + opacity: 0; 338 + pointer-events: none; 339 + } 340 + 341 + .splash-content { 342 + display: flex; 343 + flex-direction: column; 344 + align-items: center; 345 + gap: var(--spacing-lg); 346 + } 347 + 348 + .splash-logo { 349 + font-size: 2.5em; 350 + color: wheat; 351 + } 352 + 353 + .splash-spinner { 354 + font-size: 2em; 355 + color: wheat; 356 + animation: splash-spin 1.5s linear infinite; 357 + } 358 + 359 + @keyframes splash-spin { 360 + from { 361 + transform: rotate(0deg); 362 + } 363 + to { 364 + transform: rotate(360deg); 365 + } 366 + }