Introduction

Whack is an open-source, multi-media platform designed for the Rust language, used for creating rich graphical experiences.

License

While Whack is open-source with the very permissive Apache 2.0 license, it relies on a library with a slightly less permissive public license, so ensure your Whack application is distributed in compliance with their license.

Performance

Whack aims to be faster than Progressive Web Applications – which are typically implemented using HTML5, TypeScript and reactive frameworks like React.js. HTML5 applications are well-known for consuming significant RAM memory and high CPU usage. Whack apps are faster than apps made using technologies like Electron, Tauri and Cordova.

Flexibility

Whack supports a Cascading Style Sheet 3 subset, reactive user interfaces and a complex graphical display model.

Licensing obligations

This section outlines certain obligations that creators must meet when distributing their applications.

GStreamer

GStreamer, a third-party dependency used in Whack for the video player, is distributed under LGPL 2.1 (GNU LESSER GENERAL PUBLIC LICENSE).

Creators must ensure that, when distributing Whack in a binary, they:

  • Give a notice that they indirectly use GStreamer
  • Link to the GStreamer source code
  • Provide the GStreamer license file (LGPL)

GStreamer source code can be accessed at:

Their LGPL license obligates allowing users to replace the GStreamer library. In that case, the Whack packaging tool outputs GStreamer as a shared object somewhere in the app package, which users can identify and replace the file by another one.

Build requisites

Skia

Whack depends on Skia. See the rust-skia build requisites.

  • For building under Windows, your best bet is using the MSVC toolchain (not GNU, as there are no ready binaries for them).

Application descriptor

The application descriptor is written inside the Cargo manifest.

Example

[package.metadata.whack]
id = "com.business.app"
human-name = "Business App"
framerate = "60"
files = [
    { from = "foo/bar.png", to = "qux/baz.png" },
    { from = "assets/**/*.webp", to = "assets" }
]

[package.metadata.whack.initial-window]
width = 750
height = 750

Installing files

Files can be inserted into the installation directory through the files option in the application descriptor.

[package.metadata.whack]
files = [
    { from = "foo/bar.png", to = "qux/baz.png" },
    { from = "assets/**/*.webp", to = "assets" }
]

Rules:

  1. If there is a ** or * (in left-to-right order), then the initial path components until ** or * are replaced by to.
  2. If there is no ** nor *, then to represents the final file path.

Here is an example reading installation assets:

#![allow(unused)]
fn main() {
// Vec<u8>
whack::File::new("app://path/to/file").read().await.unwrap()
// String
whack::File::new("app://path/to/file").read_utf().await.unwrap()
}

Note: Currently ./ and ../ components are only recommended in the beginning of the from pattern. Do not use ../ components in the to path. Do not use ./ components other than in the beginning of the to path.

HTML5 configuration

# HTML5 configuration
[package.metadata.whack.html5]
# The World Wide Web root (default: "/");
# that is the absolute path of the application.
# This option is only necessary when the application is served
# anywhere other than the root directory in the same domain.
wwwroot = "/"

Command Line Interface

The whack command is primarily used for building Whack apps and provides additional utilities. Instance usage: users must use the whack run command for running apps in development phase instead of cargo run, otherwise unexpected behavior may occur.

Events

Both display objects and UI components emit events, which are documented in whack::events::disp::* and whack::events::comp::*.

Here are some events that display objects include:

  • added_to_stage
  • removed_from_stage
  • enter_frame (a broadcast event)
  • exit_frame (a broadcast event)
  • Pointer events

Events include tunneling information, since they are dispatched in three phases:

  1. Capturing phase (optional). Goes from the root parent to the parent of the target.
  2. Target phase. Goes at the target object.
  3. Bubbling phase (optional). Goes from the parent of the target to the root parent.

Handling DOM events

Events that are pre-defined in UiComponent and DisplayObject may be handled as follows:

#![allow(unused)]
fn main() {
// Use comp= for UI components
// (whack::components::UiComponent)
whack::evt!(comp=button, click, |e, d| {
    //
});

// Use disp= for display objects
// (whack::gfx::DisplayObject)
whack::evt!(disp=image, click, |e, d| {
    //
});
}

Note: e holds the event tunneling information and d holds event-specific details (for example, for a click event, the (x, y) coordinates and other properties).

The whack::evt! macro returns an EventListener object with a .remove() method to detach the listener from the tied DisplayObject or UiComponent.

Dynamic events may be handled using dyn=event_type, requiring an explicit detail type to cast to if used:

#![allow(unused)]
fn main() {
whack::evt!(disp=image, dyn="custom_event", |e, d: CustomEvent| {
    //
});
}

Note that the e and d parameters may be omitted if unused.

Capture

You may specify the capture option in whack::evt!, equivalent to the capture option in the WHATWG DOM [object EventTarget].addEventListener(..., { capture }).

#![allow(unused)]
fn main() {
whack::evt!(disp=obj, click, || {
    //
}, capture = true);
}

Emitting events

Here is a simple example demonstrating an .emit() method call.

#![allow(unused)]
fn main() {
obj.emit(whack::Event::new("event_name", whack::EventOptions {
    detail: Rc::new(whack::GenericEvent),
    ..default()
}));
}

Timers

For a parallel timeout, use either of:

#![allow(unused)]
fn main() {
// Option 1
let timeout = whack::easy_timeout!({
    // Action
}, duration);
// Stop whenever desired
timeout.stop();

// Option 2
future_exec(async {
    let mut ticker = whack::ticker(whack::Duration::from_secs(10));
    ticker.tick().await;
});
}

There are variants appropriate for web animations, intervals and other functions included in the timer API.

WhackDS

WhackDS (Design Suite) is the primary method through which creators extend the Document Object Model, the closed UiComponent hierarchy, with their own reactive UI components.

Relationship with other technologies

WhackDS can be thought as the "React.js" counterpart of the Whack framework. Certain advantages compared to React.js include:

  • No stale captures of states, fixtures (or React.js "refs") and context reflections.
  • Built-in style sheet integration.
  • Default memoization

Memoization

Components are memoized by default, unlike in React.js.

To customize the equality of a field during memoization, use #[eq(...)]:

#![allow(unused)]
fn main() {
#[eq(a == b)]
x: f64,
}

Example

#![allow(unused)]
fn main() {
use whack::{components::*, ds};

#[ds::component]
pub fn Component1(
    /// To which fixture to bind the contained `Label`.
    #[fixture_option]
    bind: UiComponent,
) {
    let x = ds::use_state::<f64>(|| 0.0);
    let button = ds::use_fixture<Option<UiComponent>>(|| None);

    // Effect
    ds::use_effect!({
        (*button).unwrap().focus();
    });

    ds::xml! {
        <w:VGroup s:color="orange">
            // Style sheet scoped to that VGroup tag.
            <w:Style>
            r###"
                :host {
                    background: red;
                }
            "###
            </w:Style>
            <w:Label bind={bind}>"counter: {x.get()}"</w:Label>
            <w:Button
                bind={button}
                click&={x.set(x.get() + 1);}>
                "increase x"
            </w:Button>
        </w:VGroup>
    }
}
}

<w:Style> tag

A component tag accepts <w:Style> tags only if it defines a stylesheets: Option<whack::ds::StyleSheets> field.

The creator can separate a large CSS into a file. This uses include_str!() under the hood and still uses lazy parsing for efficiency.

#![allow(unused)]
fn main() {
<w:Style source="skins/button.css"/>
}

Style tags accept parameters...

#![allow(unused)]
fn main() {
<w:Style color="red">
r###"
    :host {
        background: param(color);
    }
"###
</w:Style>
}

Or use b:x= for passing a Binary (common for icons).

The creator may spread existing style sheets:

#![allow(unused)]
fn main() {
<w:Style extend={stylesheets}/>
    // stylesheets: Option<whack::ds::StyleSheets>
}

The creator may pass a HashMap<String, String> as arguments to a style sheet...

#![allow(unused)]
fn main() {
<w:Style map={m}>
r###"
    :host {
        background: param(color);
    }
"###
</w:Style>
}

... and/or a HashMap<String, Binary> through b:map=.

Shorthand attributes

#![allow(unused)]
fn main() {
// bool
x // x={true}

// Event handlers
event&={println!("hi");}
    // equivalent to `event=fn{|event, detail|{println!("hi");}}`

// Functions
f=fn{f} // equivalent to `f={Handler(Rc::new(f))}}`

// Inline styles
s:style_property="css exp"
}

Fixtures

Fixtures are used similiarly to states, except they do not trigger a re-render of an UI component when the value is overwritten.

Note: For who is familiar with React.js, fixtures are the WhackDS equivalent of refs.

Option<whack::ds::Fixture<T>> is used for passing fixtures down to components. An actual fixture is represented by whack::ds::FixtureFixture<T>.

It is common to do the following in whack::ds::xml!:

#![allow(unused)]
fn main() {
bind={|val| {
    my_ref.set(val.clone());
    match params.bind {
        Some(whack::ds::Fixture::Fixture(ref r)) => {
            r.set(val);
        },
        // Receiver(Rc<dyn Fn(T) + 'static>)
        Some(whack::ds::Fixture::Receiver(ref f)) => {
            f(val);
        },
    }
}}
}

Fields

Component fields are the parameters it receives in the whack::ds::component attribute macro.

String

A field

#![allow(unused)]
fn main() {
x: String,
}

accepts anything impl AsRef<str>. The program might panic if unspecified.

String (optional)

A field

#![allow(unused)]
fn main() {
x: Option<String>,
}

accepts anything impl Into<Option<impl AsRef<str>>>.

In this case a more complex setter is generated, like:

#![allow(unused)]
fn main() {
pub fn set_x<S: AsRef<str>>(&self, s: impl Into<Option<S>>) {
    let s = s.into().as_ref().map(|s| s.as_ref());
    self.x.replace(s.map(|s| s.to_owned()))
}
}

Function

#![allow(unused)]
fn main() {
x: impl Fn(...),
x: impl Fn(...) -> T,
}

yields a Handler<dyn Fn(...) + 'static> field.

During memoization, equality, by default, always returns true to avoid re-rendering the component. It is assumed that these functions are always never changing.

For event handlers, you may want to have a signature like |event, detail| { ... }, so it can be used with the &= attribute notation.

Function (optional)

#![allow(unused)]
fn main() {
x: Option<impl Fn(...)>,
x: Option<impl Fn(...) -> T>,
}

yields an Option<Handler<dyn Fn(...) + 'static>> field.

During memoization, the equality comparison of the function objects always returns true by default to avoid re-rendering the component. It is assumed that these functions are always never changing.

For event handlers, you may want to have a signature like |event, detail| { ... }, so it can be used with the &= attribute notation.

Implicit

A field

#![allow(unused)]
fn main() {
x: T,
}

... accepts anything impl Into<T>. The program might panic if it is unspecified.

For primitives (like f64 and bool), generates a Cell; for other types, generates a RefCell.

A setter like this is generated:

#![allow(unused)]
fn main() {
pub fn set_x(&self, v: impl Into<T>) {
    self.x.replace(Some(v.into()));
}
}

Implicit (optional)

A field

#![allow(unused)]
fn main() {
x: Option<T>,
}

... unlike implicit fields, doesn't panic if unspecified, defaulting to None.

A setter like this is generated:

#![allow(unused)]
fn main() {
pub fn set_x<U: Into<T>>(&self, v: impl Into<Option<U>>) {
    let v = v.into().as_ref().map(|v| v.into());
    self.x.replace(v);
}
}

Fixture

A field

#![allow(unused)]
fn main() {
#[fixture]
x: T,
}

is equivalent to a whack::ds::Fixture<T> field, which is either a fixture fixture (a physical fixture) or a fixture receiver function. The program might panic if unspecified.

Fixture (optional)

A field

#![allow(unused)]
fn main() {
#[fixture_option]
x: T,
}

is equivalent to a Option<whack::ds::Fixture<T>> field, which is either a fixture fixture (a physical fixture), or a fixture receiver function, or unspecified (None).

Field default

Specifying a default value for a field prevents the program from panicking for omitting the field. Option-typed fields have None as default, so they do need an explicit default.

#![allow(unused)]
fn main() {
#[default = v]
x: T,
}

Hooks

use_state(...), use_fixture(...), use_context(...) and use_effect!(...) are examples of built-in hooks.

Hooks must only appear at the top-level of a WhackDS component, and must not be run conditionally.

Effects

The use_effect! hook (effect) is used for either:

  • Running code when component mounts and unmounts (the cleanup)
  • Running code when a list of values change (e.g. states, derived values and parameters)

The effect must explicitly state its dependencies (i.e. it should run when a specific value changes), or it has no dependencies by default.

Here are some examples:

#![allow(unused)]
fn main() {
whack::ds::use_effect!({
    // runs whenever component mounts
});

whack::ds::use_effect!({
    // runs whenever component mounts
    // (...)

    || {
        // cleanup.
        // runs when component unmounts
    }
});

whack::ds::use_effect!({
    // runs whenever the `x` state changes
}, [x.get()]);

whack::ds::use_effect!({
    // runs whenever the `variant` parameter changes
    // (...)

    || {
        // cleanup.
        // runs either before re-running this effect,
        // or when component is unmounted.
    }
}, [variant]);
}

Item key

When interpolating a list of nodes inside the xml! macro, make sure to specify the special key attribute with an unique ID for each item.

Data attributes

Native <w:*> tags constructing UI components may include arbitrary data- prefixed attributes.