Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started
All examples
RecipesDevicemedia120 lines · 1 files

Embedded web page

A native browser over the canvas, at this component's rect. On the web it is an <iframe>, and script only reaches a page from your own origin.

What this shows

Embedding a web page — the third native surface, and the one with a limit worth knowing.

WebContainer puts the platform's own browser over the GL canvas at this component's rect: WebView on Android, WKWebView on iOS, the system engine on Windows, a WKWebView through JNA on macOS, and — since 17/09/2026 — an

Compiled to WebAssembly-era JavaScript by TeaVM — the same code, not a video of it. · All examples

What it uses

Framework types this example touches. The linked ones have a reference page.

ButtonCapabilityCardComponentDebugDeviceGridLayoutLabelPlatformUtilsScreenStylesVerticalLayoutWebContainerWidgets

The source

Complete and unabridged. This is the file, not an excerpt of it.

Raw
dev/cf/recipe/R33WebView.java
package dev.cf.recipe;

import dev.cf.Tones;

import dev.crossfly.Capability;
import dev.crossfly.PlatformUtils;
import dev.crossfly.util.Debug;
import dev.crossfly.ui.*;
import dev.crossfly.ui.layout.*;
import dev.crossfly.ui.style.*;

import static dev.crossfly.ui.Widgets.*;

/**
 * Embedding a web page — the third native surface, and the one with a limit worth knowing.
 *
 * <p>{@link WebContainer} puts the platform's own browser over the GL canvas at this component's
 * rect: {@code WebView} on Android, {@code WKWebView} on iOS, the system engine on Windows, a
 * {@code WKWebView} through JNA on macOS, and — since 17/09/2026 — an {@code <iframe>} on the web,
 * which had been the only one of the five without one.</p>
 *
 * <p><b>The limit is on the web and it is not a defect.</b> The four native backends can inject
 * script into any page they load. A browser cannot: reaching into a frame from another origin
 * raises {@code SecurityError}, by design, with no flag that changes it. So
 * {@link WebContainer#evaluateJavaScript} works on a page from the app's own origin and is refused
 * — out loud, in the log — on anything else. The page talks back the way a framed page always has,
 * with {@code postMessage}, and it arrives at the same listener the native bridge feeds.</p>
 *
 * <p>Two more things that bite whoever embeds one for the first time, on every platform: a native
 * surface does not clip to a scrolling parent and shows as a blank rectangle during a window
 * transition; and plenty of sites refuse to be framed at all ({@code X-Frame-Options}), which looks
 * exactly like a page that did not load, because from outside the frame it is indistinguishable.</p>
 */
public class R33WebView extends Recipe
{
   private Label answer;
   private WebContainer web;

   public String title()
   {
      return "Embedded web page";
   }

   public String note()
   {
      return "A native browser over the canvas, at this component's rect. On the web it is an"
         + " <iframe>, and script only reaches a page from your own origin.";
   }

   public void build(Component body) throws Exception
   {
      boolean has = PlatformUtils.isSupported(Capability.WEB_VIEW);

      answer = label(has ? "Ready." : "This platform has no embedded web view.", 1f, false,
         has ? Styles.captionColor : Tones.danger(), Position.START);
      answer.testTag("webview.answer");
      // Pinned below the scrolling body, so the answer stays in view (see Recipe.pin).
      pin(answer);

      if (!has)
      {
         Card c = new Card();
         c.setLayout(new VerticalLayout(Position.START, Position.START));
         c.add(LAY_AUTO, label("No WEB_VIEW on " + dev.crossfly.device.Device.platform, 1.2f, true,
            Styles.textColor, Position.START));
         c.add(LAY_AUTO, label("Ask the capability before building a WebContainer: its constructor"
            + " refuses by name, which is what lets an app open the page in the system browser"
            + " instead.", 1f, false, Styles.captionColor, Position.START));
         body.add(LAY_AUTO, c);
         return;
      }

      web = new WebContainer();
      // The page reaches the app through one listener on every platform: the native backends inject
      // window.DesktopBridge, and the web build installs the same name into a same-origin page.
      web.setFormSubmitListener(data -> said("the page said: " + data));
      body.add(.6f, web);

      Component buttons = new Component();
      buttons.setLayout(new GridLayout(Position.START, Position.CENTER));
      buttons.add(.33f, .33f, .33f,
         button("Load", "webview.load", () ->
         {
            web.show("https://example.com");
            said("Loading example.com…");
         }),
         button("Ask the page", "webview.ask", () ->
         {
            // On the four native platforms this comes back through the bridge. On the web it is
            // refused, because example.com is another origin -- and the refusal is logged rather
            // than being the silence that would send you debugging the page.
            web.evaluateJavaScript("window.DesktopBridge && window.DesktopBridge.onFormSubmit("
               + "'title=' + document.title)");
            said("Asked. On the web this is refused unless the page is from this origin.");
         }),
         button("Hide", "webview.hide", () ->
         {
            web.hide();
            said("Hidden — the page stays loaded.");
         }));
      body.add(LAY_AUTO, buttons);
   }

   private Button button(String text, String tag, Runnable go) throws Exception
   {
      Button b = new Button(text);
      b.testTag(tag);
      b.onAction(e -> go.run());
      return b;
   }

   private void said(String what)
   {
      Debug.log("[webview] " + what);
      answer.setText(what);
      answer.invalidateMeasure(); // the pinned band must grow with it (see Recipe.pin)
      Screen.repaint();
   }
}
For an AI assistant

Machine-readable versions of this page, so a model can read the shelf instead of guessing at the API.

This example, as JSONThis example, as Markdown
PreviousVideo playbackNextReading an odometer (OCR)