j4rs: JavaFX support (WIP)

Note: JavaFX support in j4rs is a work in progress. Adding features as the time passes and versioning evolves seems better than attempting to create full-feature JavaFX support for Rust. The latter feels rather difficult, frightening and time-demanding.

Introduction

Some time ago, I was exploring things that can be achieved by using j4rs and had the idea to implement a JavaFX GUI. This indeed happened, but the attempt included implementing some parts in Java and some in Rust, along with several hacks.

With this JavaFX support WIP, there is no need to write Java code any more and things are simpler. Of course, a prerequisite is the knowledge of JavaFX. j4rs does not create an API or DSL above the JavaFX API, like, for example ScalaFX does.

The approach for now is to use j4rs to manipulate the JavaFX API itself, while implementing special Rust API calls when needed.

Steps to build a JavaFX UI

1. Have Rust, cargo and JDK 11 (or above) installed

2. Retrieve the JavaFX dependencies for j4rs:

A good idea is that this happens during build time, in order the dependencies to be available when the actual Rust application starts and the JVM is initialized. This can happen by adding the following in a build script:

	use j4rs::JvmBuilder;
	use j4rs::jfx::JavaFxSupport;

	fn main() {
		let jvm = JvmBuilder::new().build().unwrap();
		jvm.deploy_javafx_dependencies().unwrap();
	}

3. Implement the UI:

There are two choices here; either build the UI using FXML, or, build it traditionally, using Java code. In the code snippets below, you may find comments with a short description for each line.

3.a Implement the UI with Java calls to the JavaFX API

// Create a Jvm with JavaFX support
let jvm = JvmBuilder::new().with_javafx_support().build()?;

// Start the JavaFX application.
// When the JavaFX application starts, the `InstanceReceiver` channel that is returned from the `start_javafx_app` invocation
// will receive an Instance of `javafx.stage.Stage`.
// The UI may start being built using the provided `Stage`.
let stage = jvm.start_javafx_app()?.rx().recv()?;

// Create a StackPane. Java code: StackPane root = new StackPane();
let root = jvm.create_instance("javafx.scene.layout.StackPane", &[])?;

// Create the button. Java code: Button btn = new Button();
let btn = jvm.create_instance("javafx.scene.control.Button", &[])?;
// Get the action channel for this button
let btn_action_channel = jvm.get_javafx_event_receiver(&btn, FxEventType::ActionEvent_Action)?;
// Set the text of the button. Java code: btn.setText("Say Hello World to Rust");
jvm.invoke(&btn, "setText", &["A button that sends events to Rust".try_into()?])?;
// Add the button to the GUI. Java code: root.getChildren().add(btn);
jvm.chain(&root)?
	.invoke("getChildren", &[])?
	.invoke("add", &[btn.try_into()?])?
	.collect();

// Create a new Scene. Java code: Scene scene = new Scene(root, 300, 250);
let scene = jvm.create_instance("javafx.scene.Scene", &[
	root.try_into()?,
	InvocationArg::try_from(300_f64)?.into_primitive()?,
	InvocationArg::try_from(250_f64)?.into_primitive()?])?;
// Set the title for the scene. Java code: stage.setTitle("Hello Rust world!");
jvm.invoke(&stage, "setTitle", &["Hello Rust world!".try_into()?])?;
// Set the scene in the stage. Java code: stage.setScene(scene);
jvm.invoke(&stage, "setScene", &[scene.try_into()?])?;
// Show the stage. Java code: stage.show();
jvm.invoke(&stage, "show", &[])?;

3.b Implement the UI with FXML

I personally prefer building the UI with FXMLs, using for example the Scene Builder.

The thing to keep in mind is that the controller class should be defined in the root FXML element and it should be fx:controller="org.astonbitecode.j4rs.api.jfx.controllers.FxController"

Here is an FXML example; it creates a window with a label and a button:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>

<VBox alignment="TOP_CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="725.0" spacing="33.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.astonbitecode.j4rs.api.jfx.controllers.FxController">
   <children>
      <Label text="JavaFX in Rust">
         <font>
            <Font size="65.0" />
         </font>
      </Label>
      <Label text="This UI is loaded with a FXML file" />
      <HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0" spacing="10.0">
         <children>
            <Button id="helloButton" mnemonicParsing="false" text="Say Hello" />
         </children>
      </HBox>
   </children>
</VBox>

The id of the elements can be used to retrieve the respective Nodes in Rust and act upon them (eg. adding Event Listeners, changing the texts or effects on them etc).

// Create a Jvm with JavaFX support
let jvm = JvmBuilder::new().with_javafx_support().build()?;

// Start the JavaFX application.
// When the JavaFX application starts, the `InstanceReceiver` channel that is returned from the `start_javafx_app` invocation
// will receive an Instance of `javafx.stage.Stage`.
// The UI may start being built using the provided `Stage`.
let stage = jvm.start_javafx_app()?.rx().recv()?;

// Set the title for the scene. Java code: stage.setTitle("Hello Rust world!");
jvm.invoke(&stage, "setTitle", &["Hello JavaFX from Rust!".try_into()?])?;
// Show the stage. Java code: stage.show();
jvm.invoke(&stage, "show", &[])?;

// Load a fxml. This returns an `FxController` which can be used in order to find Nodes by their id,
// add Event Listeners and more.
let controller = jvm.load_fxml(&PathBuf::from("./fxml/jfx_in_rust.fxml"), &stage)?;

// Wait for the controller to be initialized. This is not mandatory, it is here to shoe that the functionality exists.
let _ = controller.on_initialized_callback(&jvm)?.rx().recv()?;
println!("The controller is initialized!");

// Get the InstanceReceiver to retrieve callbacks from the JavaFX button with id helloButton
let hello_button_action_channel = controller.get_event_receiver_for_node("helloButton", FxEventType::ActionEvent_Action, &jvm)?;


The j4rs-showcase repo is updated with the above approaches.

TODO:

  • fxml support

  • Extend the API for more JavaFX functionality support

  • Allow more options regarding the JavaFX version to use underneath


You are welcome to share any thoughts, improvements or proposals.

Thanks for reading!

rust  java  j4rs  javafx