diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93c8474 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# IntelliJ +*.iml +.idea/ + +# Eclipse +.settings/ +.project +.classpath +.metadata + +# Common +target/ +.DS_Store +__MACOSX + +/webview* +/libwebview* +/WebView* diff --git a/Example.java b/Example.java new file mode 100644 index 0000000..8607400 --- /dev/null +++ b/Example.java @@ -0,0 +1,19 @@ +package dev.webview; + +public class Example { + + public static void main(String[] args) { + Webview wv = new Webview(); // Can optionally be created with an AWT component to be painted on. + + // Calling `await echo(1,2,3)` will return `[1,2,3]` + wv.bind("echo", (arguments) -> { + return arguments; + }); + + wv.loadURL("https://google.com"); + + // Run the webview event loop, the webview is fully disposed when this returns. + wv.run(); + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..abdb64d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Webview + +A (new!) Java port of the [webview project](https://github.com/webview/webview). It uses JNA under the hood to interface with the webview library. + +## How to use + +1) Include the libary in your project (see the [JitPack page](https://jitpack.io/#Casterlabs/webview)). +2) Copy and run the example in `Example.java`. +3) Profit! + +## TODO + +Build our own DLLs and whatnot, the current ones are copied from the C# port. \ No newline at end of file diff --git a/SwingExample.java b/SwingExample.java new file mode 100644 index 0000000..8c084b5 --- /dev/null +++ b/SwingExample.java @@ -0,0 +1,47 @@ +package dev.webview; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import javax.swing.JFrame; + +public class SwingExample { + + public static void main(String[] args) { + JFrame frame = new JFrame(); + + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setLayout(new BorderLayout()); + + // Using createAWT allows you to defer the creation of the webview until the + // canvas is fully renderable. + Component component = Webview.createAWT((wv) -> { + // Calling `await echo(1,2,3)` will return `[1,2,3]` + wv.bind("echo", (arguments) -> { + return arguments; + }); + + wv.loadURL("https://google.com"); + + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + wv.close(); + frame.dispose(); + System.exit(0); + } + }); + + // Run the webview event loop, the webview is fully disposed when this returns. + wv.run(); + }); + + frame.getContentPane().add(component, BorderLayout.CENTER); + + frame.setSize(800, 600); + frame.setVisible(true); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..faab5fe --- /dev/null +++ b/pom.xml @@ -0,0 +1,120 @@ + + 4.0.0 + dev.webview + Webview + 1.0.0 + + + UTF-8 + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.3.2 + + ${project.name}-original + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + shade + package + + shade + + + + + true + ${project.name} + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + ${project.name} + + + + attach-sources + + jar + + + + + + + + + + jitpack.io + https://jitpack.io + + + + + + + + org.projectlombok + lombok + 1.18.22 + provided + + + + org.jetbrains + annotations + 19.0.0 + provided + + + + com.github.casterlabs.rakurai + Json + 1.13.0 + compile + + + com.github.casterlabs.rakurai + Util + 1.13.0 + compile + + + + net.java.dev.jna + jna + 5.10.0 + compile + + + net.java.dev.jna + jna-platform + 5.10.0 + compile + + + + + \ No newline at end of file diff --git a/src/main/java/dev/webview/ConsumingProducer.java b/src/main/java/dev/webview/ConsumingProducer.java new file mode 100644 index 0000000..cd88376 --- /dev/null +++ b/src/main/java/dev/webview/ConsumingProducer.java @@ -0,0 +1,17 @@ +package dev.webview; + +import org.jetbrains.annotations.Nullable; + +import lombok.NonNull; + +public interface ConsumingProducer { + + public @Nullable P produce(@Nullable C consume) throws InterruptedException; + + public static ConsumingProducer of(@NonNull Class consumingClazz, @Nullable P result) { + return (aVoid) -> { + return result; + }; + } + +} diff --git a/src/main/java/dev/webview/Pair.java b/src/main/java/dev/webview/Pair.java new file mode 100644 index 0000000..27de6b7 --- /dev/null +++ b/src/main/java/dev/webview/Pair.java @@ -0,0 +1,10 @@ +package dev.webview; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public class Pair { + public final A a; + public final B b; + +} diff --git a/src/main/java/dev/webview/Platform.java b/src/main/java/dev/webview/Platform.java new file mode 100644 index 0000000..fb81a58 --- /dev/null +++ b/src/main/java/dev/webview/Platform.java @@ -0,0 +1,35 @@ +package dev.webview; + +import java.util.regex.Pattern; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum Platform { + // @formatter:off + MACOSX ("macOS", "mac|darwin"), + LINUX ("Linux", "nux"), + WINDOWS ("Windows", "win"); + // @formatter:on + + private String str; + private String regex; + + static Platform get() { + String osName = System.getProperty("os.name").toLowerCase(); + + for (Platform os : values()) { + if (Pattern.compile(os.regex).matcher(osName).find()) { + return os; + } + } + + throw new UnsupportedOperationException("Unknown operating system: " + osName); + } + + @Override + public String toString() { + return this.str; + } + +} \ No newline at end of file diff --git a/src/main/java/dev/webview/Webview.java b/src/main/java/dev/webview/Webview.java new file mode 100644 index 0000000..652cdf8 --- /dev/null +++ b/src/main/java/dev/webview/Webview.java @@ -0,0 +1,193 @@ +package dev.webview; + +import static dev.webview.WebviewNative.NULL_PTR; +import static dev.webview.WebviewNative.WV_HINT_FIXED; +import static dev.webview.WebviewNative.WV_HINT_MAX; +import static dev.webview.WebviewNative.WV_HINT_MIN; +import static dev.webview.WebviewNative.WV_HINT_NONE; + +import java.awt.Canvas; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics; +import java.io.Closeable; +import java.util.function.Consumer; + +import org.jetbrains.annotations.Nullable; + +import com.sun.jna.Native; +import com.sun.jna.ptr.PointerByReference; + +import co.casterlabs.rakurai.json.Rson; +import co.casterlabs.rakurai.json.element.JsonArray; +import co.casterlabs.rakurai.json.element.JsonElement; +import co.casterlabs.rakurai.json.serialization.JsonParseException; +import dev.webview.WebviewNative.BindCallback; +import lombok.NonNull; + +public class Webview implements Closeable, Runnable { + private static final WebviewNative N; + + public static final Platform PLATFORM = Platform.get(); + + static { + String toLoad = WebviewNative.runSetup(); + N = Native.load(toLoad, WebviewNative.class); + } + + @Deprecated + public long $pointer; + + public static Component createAWT(@NonNull Consumer onCreate) { + return new Canvas() { + private static final long serialVersionUID = 5199512256429931156L; + + private boolean initialized = false; + + private Webview webview; + private Dimension lastSize = null; + + @Override + public void paint(Graphics g) { + Dimension size = this.getSize(); + + if (!size.equals(this.lastSize)) { + this.lastSize = size; + + if (this.webview != null) { + this.updateSize(); + } + } + + if (!this.initialized) { + this.initialized = true; + + new Thread(() -> { + this.webview = new Webview(this); + + this.updateSize(); + + onCreate.accept(this.webview); + }).start(); + } + } + + private void updateSize() { + int width = this.lastSize.width; + int height = this.lastSize.height; + + // There is a random margin on Windows that isn't visible, so we must + // compensate. + // TODO figure out why this is caused. + if (PLATFORM == Platform.WINDOWS) { + width -= 16; + height -= 39; + } + + this.webview.setFixedSize(width, height); + } + + }; + } + + /** + * Creates a new Webview. + */ + public Webview() { + this(NULL_PTR); + } + + /** + * Creates a new Webview. + * + * @param target The target awt component, such as a {@link java.awt.JFrame} or + * {@link java.awt.Canvas} + */ + public Webview(@NonNull Component target) { + this(new PointerByReference(Native.getComponentPointer(target))); + } + + /** + * @deprecated Use this if you absolutely do know what you're doing. + */ + @Deprecated + public Webview(@Nullable PointerByReference windowPointer) { + $pointer = N.webview_create(false, windowPointer); + + this.loadURL(null); + } + + public void loadURL(@Nullable String url) { + if (url == null) { + url = "about:blank"; + } + + N.webview_navigate($pointer, url); + } + + public void setTitle(@NonNull String title) { + N.webview_set_title($pointer, title); + } + + public void setMinSize(int width, int height) { + N.webview_set_size($pointer, width, height, WV_HINT_MIN); + } + + public void setMaxSize(int width, int height) { + N.webview_set_size($pointer, width, height, WV_HINT_MAX); + } + + public void setSize(int width, int height) { + N.webview_set_size($pointer, width, height, WV_HINT_NONE); + } + + public void setFixedSize(int width, int height) { + N.webview_set_size($pointer, width, height, WV_HINT_FIXED); + } + + public void setInitScript(@NonNull String script) { + N.webview_init($pointer, script); + } + + public void eval(@NonNull String script) { + N.webview_eval($pointer, script); + } + + public void bind(@NonNull String name, @NonNull ConsumingProducer handler) { + N.webview_bind($pointer, name, new BindCallback() { + @Override + public void callback(long seq, String req, long arg) { + try { + JsonArray arguments = Rson.DEFAULT.fromJson(req, JsonArray.class); + + try { + @Nullable + JsonElement result = handler.produce(arguments); + + N.webview_return($pointer, seq, false, Rson.DEFAULT.toJsonString(result)); + } catch (Exception e) { + N.webview_return($pointer, seq, true, e.getMessage()); + } + } catch (JsonParseException e) { + e.printStackTrace(); + } + } + }, 0); + } + + public void unbind(@NonNull String name) { + N.webview_unbind($pointer, name); + } + + @Override + public void run() { + N.webview_run($pointer); + N.webview_destroy($pointer); + } + + @Override + public void close() { + N.webview_terminate($pointer); + } + +} diff --git a/src/main/java/dev/webview/WebviewNative.java b/src/main/java/dev/webview/WebviewNative.java new file mode 100644 index 0000000..84725a8 --- /dev/null +++ b/src/main/java/dev/webview/WebviewNative.java @@ -0,0 +1,202 @@ +package dev.webview; + +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; + +import com.sun.jna.Callback; +import com.sun.jna.Library; +import com.sun.jna.ptr.PointerByReference; + +import co.casterlabs.rakurai.io.IOUtil; +import lombok.NonNull; +import lombok.SneakyThrows; + +public interface WebviewNative extends Library { + static final PointerByReference NULL_PTR = null; + + @SneakyThrows + static String runSetup() { + String toLoad = "libwebview"; + + String[] libraries = null; + + switch (Webview.PLATFORM) { + case LINUX: { + libraries = new String[] { + "libwebview.so" + }; + break; + } + + case MACOSX: { + libraries = new String[] { + "libwebview.dylib" + }; + break; + } + + case WINDOWS: { + libraries = new String[] { + "webview.dll", + "WebView2Loader.dll" + }; + toLoad = "webview"; + break; + } + } + + // Extract all of the libs. + for (String lib : libraries) { + File file = new File(lib); + + if (!file.exists()) { + InputStream in = WebviewNative.class.getResourceAsStream("/" + lib); + byte[] bytes = IOUtil.readInputStreamBytes(in); + Files.write(file.toPath(), bytes); + } + } + + return toLoad; + } + + static final int WV_HINT_NONE = 0; + static final int WV_HINT_MIN = 1; + static final int WV_HINT_MAX = 2; + static final int WV_HINT_FIXED = 3; + + /** + * Used in {@link webview_bind} + */ + static interface BindCallback extends Callback { + + /** + * @param seq The request id, used in {@link webview_return} + * @param req The javascript arguments converted to a json array (string) + * @param arg Unused + */ + void callback(long seq, String req, long arg); + + } + + /** + * Creates a new webview instance. If debug is true - developer tools will be + * enabled (if the platform supports them). Window parameter can be a pointer to + * the native window handle. If it's non-null - then child WebView is embedded + * into the given parent window. Otherwise a new window is created. Depending on + * the platform, a GtkWindow, NSWindow or HWND pointer can be passed here. + * + * @param debug Enables developer tools if true (if supported) + * @param $window A pointer to a native window handle, for embedding the webview + * in a window. (Either a GtkWindow, NSWindow, or HWND pointer) + */ + long webview_create(boolean debug, PointerByReference window); + + /** + * @return a native window handle pointer. + * + * @param $pointer The instance pointer of the webview + * + * @implNote This is either a pointer to a GtkWindow, NSWindow, or + * HWND. + */ + long webview_get_window(long $pointer); + + /** + * Navigates to the given URL. + * + * @param $pointer The instance pointer of the webview + * @param url The target url, can be a data uri. + */ + void webview_navigate(long $pointer, String url); + + /** + * Sets the title of the webview window. + * + * @param $pointer The instance pointer of the webview + * @param title + */ + void webview_set_title(long $pointer, String title); + + /** + * Updates the webview's window size, see {@link WV_HINT_NONE}, + * {@link WV_HINT_MIN}, {@link WV_HINT_MAX}, and {@link WV_HINT_FIXED} + * + * @param $pointer The instance pointer of the webview + * @param width + * @param height + * @param hint + */ + void webview_set_size(long $pointer, int width, int height, int hint); + + /** + * Runs the main loop until it's terminated. You must destroy the webview after + * this method returns. + * + * @param $pointer The instance pointer of the webview + */ + void webview_run(long $pointer); + + /** + * Destroys a webview and closes the native window. + * + * @param $pointer The instance pointer of the webview + */ + void webview_destroy(long $pointer); + + /** + * Stops the webview loop, which causes {@link #webview_run(long)} to return. + * + * @param $pointer The instance pointer of the webview + */ + void webview_terminate(long $pointer); + + /** + * Evaluates arbitrary JavaScript code asynchronously. + * + * @param $pointer The instance pointer of the webview + * @param js The script to execute + */ + void webview_eval(long $pointer, @NonNull String js); + + /** + * Injects JavaScript code at the initialization of the new page. + * + * @implSpec It is guaranteed to be called before window.onload. + * + * @param $pointer The instance pointer of the webview + * @param js The script to execute + */ + void webview_init(long $pointer, @NonNull String js); + + /** + * Binds a native callback so that it will appear under the given name as a + * global JavaScript function. Internally it uses webview_init(). + * + * @param $pointer The instance pointer of the webview + * @param name The name of the function to be exposed in Javascript + * @param callback The callback to be called + * @param arg Unused + */ + void webview_bind(long $pointer, @NonNull String name, BindCallback callback, long arg); + + /** + * Remove the native callback specified. + * + * @param $pointer The instance pointer of the webview + * @param name The name of the callback + */ + void webview_unbind(long $pointer, @NonNull String name); + + /** + * Allows to return a value from the native binding. Original request pointer + * must be provided to help internal RPC engine match requests with responses. + * + * @param $pointer The instance pointer of the webview + * @param name The name of the callback + * @param isError Whether or not `result` should be thrown as an exception + * @param result The result (in json) + */ + void webview_return(long $pointer, long seq, boolean isError, String result); + +} diff --git a/src/main/resources/WebView2Loader.dll b/src/main/resources/WebView2Loader.dll new file mode 100644 index 0000000..2805c27 Binary files /dev/null and b/src/main/resources/WebView2Loader.dll differ diff --git a/src/main/resources/libwebview.dylib b/src/main/resources/libwebview.dylib new file mode 100644 index 0000000..3b7a87e Binary files /dev/null and b/src/main/resources/libwebview.dylib differ diff --git a/src/main/resources/libwebview.so b/src/main/resources/libwebview.so new file mode 100644 index 0000000..3cb95a8 Binary files /dev/null and b/src/main/resources/libwebview.so differ diff --git a/src/main/resources/webview.dll b/src/main/resources/webview.dll new file mode 100644 index 0000000..6408593 Binary files /dev/null and b/src/main/resources/webview.dll differ