#mobile #widgets #ui #bindings #build-environment

floui

A SwiftUI inspired native wrapper for iOS and Android widgets

6 releases

0.1.5 Jul 18, 2022
0.1.4 Jul 14, 2022

#912 in GUI

MIT license

89KB
2K SLoC

Rust 1.5K SLoC // 0.0% comments Objective-C++ 351 SLoC // 0.0% comments C++ 264 SLoC // 0.0% comments Shell 1 SLoC

floui-rs

Rust bindings for floui, pronounced "flowy", a proof-of-concept single header C++17 lib inspired by SwiftUI, which wraps native iOS and Android controls/widgets, and integrates into the de facto build environments of each platform (XCode and Android Studio).

image

Currently available controls:

  • Text
  • TextField
  • Button
  • VStack (Vertical UIStackView on iOS and LinearLayout on Android)
  • HStack (Horizontal UIStackView on iOS and LinearLayout on Android)
  • Spacer
  • Toggle/Check (tvOS doesn't support it)
  • Slider (tvOS doesn't support it)
  • ImageView
  • WebView
  • ScrollView

Usage

You can check out the floui-rs-template, which is structured to be able to build for both ios or android from the command-line.

If you would like to build purely for iOS and only using Rust (no Objective-C), check the example here.

Otherwise, if you would like to do it manually:

Build your library as a static-lib:

# Cargo.toml
[lib]
crate-type = ["static-lib"]

[dependencies]
floui = "0.1"

Build against the android and ios architectures you might need.

Rust

use floui::{enums::*, prelude::*, widgets::*};
use std::cell::RefCell;
use std::rc::Rc;

fn mygui(vc: &ViewController) -> MainView {
    let count = Rc::from(RefCell::from(0));
    MainView::new(
        &vc,
        &[
            &Button::new("Increment").foreground(Color::Blue).action({
                let count = count.clone();
                move |_| {
                    log("Increment clicked");
                    let mut c = count.borrow_mut();
                    *c += 1;
                    let t: Text = from_id("mytext").unwrap();
                    t.text(&format!("{}", c));
                }
            }),
            &Text::new("0").id("mytext").center().bold(),
            &Button::new("Decrement")
                .foreground(Color::Red)
                .action(move |_| {
                    log("Decrement clicked");
                    let mut c = count.borrow_mut();
                    *c -= 1;
                    let t: Text = from_id("mytext").unwrap();
                    t.text(&format!("{}", c));
                }),
        ],
    )
}

use std::os::raw::c_void;

#[no_mangle]
extern "C" fn floui_handle_events(arg1: *mut c_void) {
    unsafe { ViewController::handle_events(arg1); }
}

#[no_mangle]
extern "C" fn floui_main(arg1: *mut c_void, arg2: *mut c_void, arg3: *mut c_void) -> *mut c_void {
    let vc = unsafe { ViewController::new(arg1, arg2, arg3) };
    mygui(&vc).underlying() as _
}

Notes on certain usages

  • Sliders on Android take the full width of the LinearLayout, so this must be taken into consideration if code is shared also with iOS.

  • Adding images has to be in the project's resource file.

    • In Android Studio: Resource Manager, Import Drawables. This will add the file to res/drawable. The file can be accessed directly ImageView::load("MyImage.jpg").
    • In XCode: You can simply drag images into Assets.xcassets, then the image can be accessed directly ImageView::load("MyImage.jpg").
  • Using the WebView widget

    • on iOS:
      • Requires adding WebKit.framework under General > Frameworks, Libraries and Embedded Content.
      • Requires enabling the ios-webview flag in your Cargo.toml.
      • Local files can be loaded using WebView::load_url() but need to be preceded by file:///, the files should be added to your xcode project.
    • On Android:
      • To load local files, precede them with file:/// and the path of the file, which should be added to an assets folder (File > New > Folder > Assets folder). This then can be loaded using WebView::load_url().
      • To load http requests, you need to enable the internet permission in your AndroidManifest.xml: <uses-permission android:name="android.permission.INTERNET" />

Creating new widgets

Wrapping platform widgets doesn't require using C++, it requires that the widget implements WidgetExt, and the underlying (JNI jobject pointer for Android, UIView on iOS) can be retrieved. You can use the jni-rs crate and objc crates for such purposes.

Target-specific structure

iOS

  • Add the required ios rustup targets.
  • Install cargo-lipo.
  • Add the built library to your xcode project (under Build Phases > Link Binary with Libraries).
  • Modify the library search path to find the library (under Build Settings > Library Search Paths).
  • Modify your ViewController.m file:
#import "ViewController.h"

extern void *floui_main(void *, void *, void *);

@interface ViewController ()

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    floui_main((void *)CFBridgingRetain(self), nil, nil);
}

@end

Android

  • ANDROID_SDK_ROOT should be set to your android sdk directory.
  • ANDROID_NDK_ROOT should be set to your android ndk directory.
  • Add the required android rustup targets.
  • Create an Android Studio Native C++ project, choose toolchain C++ 17 in the last step.
  • Modify your CMakeLists.txt:
cmake_minimum_required(VERSION 3.4.1)
project(myapplication)

find_library(log-lib log)

add_library(myapplication SHARED native-lib.cpp)
add_library(rust-lib STATIC IMPORTED)

if (ANDROID_ABI STREQUAL x86)
    set(RUST_ARCH i686-linux-android)
elseif (ANDROID_ABI STREQUAL armeabi-v7a)
    set(RUST_ARCH armv7-linux-androideabi)
elseif (ANDROID_ABI STREQUAL arm64-v8a)
    set(RUST_ARCH aarch64-linux-android)
elseif (ANDROID_ABI STREQUAL x86_64)
    set(RUST_ARCH x86_64-linux-android)
else ()
    message(FATAL "Unknown architecture")
endif ()

set_property(TARGET rust-lib PROPERTY IMPORTED_LOCATION_DEBUG ${CMAKE_CURRENT_LIST_DIR}/app/target/${RUST_ARCH}/debug/libapp.a)
set_property(TARGET rust-lib PROPERTY IMPORTED_LOCATION_RELEASE ${CMAKE_CURRENT_LIST_DIR}/app/target/${RUST_ARCH}/release/libapp.a)

target_link_libraries(myapplication android rust-lib ${log-lib})
  • Modify your C++ file to just call the Rust lib.
#include <jni.h>
#include <string>

extern "C" void *floui_main(void *, void *, void *);

extern "C" void floui_handle_events(void *);

extern "C" JNIEXPORT jobject JNICALL
Java_com_example_myapplication_MainActivity_mainView(JNIEnv* env, jobject main_activity, jobject view) {
    return (jobject) floui_main(env, main_activity, view);
}

extern "C" JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_handleEvent(JNIEnv *env, jobject thiz, jobject view) {
    floui_handle_events(view);
}
  • Modify your MainActivity.java to look like:
package com.example.myapplication;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;

import android.os.Bundle;
import android.view.View;

import com.google.android.material.slider.Slider;

public class MainActivity extends AppCompatActivity implements View.OnClickListener, Slider.OnChangeListener {
    static {
        System.loadLibrary("myapplication");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConstraintLayout layout = new ConstraintLayout(this);
        setContentView(layout);
        mainView(layout);
    }
    public native View mainView(View view);
    public native void handleEvent(View view);

    @Override
    public void onClick(View view) {
        handleEvent(view);
    }

    @Override
    public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
        handleEvent(slider);
    }
}

Dependencies

~235KB