Nerf is (yet another) rust GUI lib

Overview

NERF

Nerf is (yet another) rust GUI lib. It is heavily inspired by Flutter, and is designed to build apps that could run on any plateforms, such as windows / linux / macOS, but also web, android, etc.

It was created after seing how huge and complex other GUI libs were. The simple counter example with Iced is 8MB, and the whole repo is 60k lines of code. When I tried it out, the compile time was huge. The counter Nerf example is 5K lines of code (when I write this readme, this will increase in the future) and the executable is 2MB.

I wanted something simple and straightforward. A lib that anyone could dive into, and understand how it works.

Features

For now, Nerf is in early development. Therefore, only a few types of widgets are currently supported. The main features are:

  • Widget tree system
  • Basic widget rendering
  • Text rendering

Dependencies

Nerf has a few dependencies, but most of them are optionnal and can be included with features.

  • winit: window creation and management, essential.
  • softbuffer: provides a 2D pixel buffer to draw on from the winit handle.
  • skia: draw engine. It is enabled by default, but can be disabled with the --no-default-features flag. When disabled, the most basic rendering operations have fallbacks, but they are much slower. It is recommanded to use skia, unless executable size should as small as possible.
  • cosmic-text: text rendering. This is disabled bu default, and any application that uses text rendering should add it. It does considerably increase the executable size.

How to use

Installation

Nerf is not yet published on crates.io, so you have to clone the repo and reference it from your project. This can easily be done in the Toml file:

[dependencies]
nerf = { path = "path/to/nerf" }

Basic usage

At it's core, nerf is not much more than a widget tree. The one important trait is the Widget trait, which defines a widget behaviour. The Widget trait has three methods:

draw:

fn draw(&self, canvas: &mut Canvas, rect: softbuffer::Rect);

The draw function is called whenever the application requests a redraw. There are usually to implementations:

  • some widgets will actually draw something on the canvas
  • others will recursively call the draw function on their children

min_space_requirements:

fn min_space_requirements(&self) -> (WidgetSizeRequirement, WidgetSizeRequirement);

This allows to have information on how widgets wished to be layout. This will allow the app to give them the required space, if available. It is important to note that this can not always be respected, and widgets should be able to handle smaller sizes. We can't stop the app user to make the window smaller than the minimum size of our widgets.

handle_event:

fn handle_event(&mut self, event: InputEvent, rect: softbuffer::Rect) -> EventResponse;

The handle event function is called whenever an event is received. It should be recursively called on all children. When a widget uses that event, they must notifiy the parent by returning a event response flags, such as the request redraw for example.

With Nerf, the idea is that widets implement their logic and own their data. Any desired behaviour is made by creating a widget, and adding it to the widget tree. The widget tree is then passed to the application, which will handle the rendering and events. For instance, a connection page will allow the user to connect, while the connected page wille store the user info. Therefore, if an app need data anytime, the best place to store it is in a custom root widget.

Example

Let's implement the classic counter example (the full example is in examples/counter.rs).

First, let's create a widget that will contain our data.

struct Counter {
    // state of the widget
    count: u32,
}

While we could implement manually the drawing, event handling of our counter, Nerf provides basic widgets that can be used. Our counter will therefore have two childs, a text widget to display the count, and a button to increment the count.

struct Counter {
    // state of the widget
    count: u32,
    // the button that will throw callbacks
    button: Box<dyn Widget>,
    // the display text. We keep it here, to be able to reference and mofify it.
    text: Box<Text>,
}

Here, the button is a dyn widget, because we don't care a lot about it. We kept the text's strong type to change it's value later.

Now, let's implement the widget trait for our counter.

impl Widget for Counter {
    [...]
}

We'll start with the draw function. All we need to draw is the text. However, we'll add a background color to make it more visible. As we kept a reference to the text widget, we can't have it be a child of our background: widgets have a unique owner, and the text is either a child of our counter or a child of a background. We will draw our background behind the button, which is a widget that does not get drawn, and only have a behaviour. Therefore, we will draw the button and the text.

fn draw(&self, canvas: &mut Canvas, rect: softbuffer::Rect) {
    self.button.draw(canvas, rect);
    self.text.draw(canvas, rect);
}

See how simple this is ? The app will take care of the layout, and will provide us with the rect to draw in. More detail on this with the next function:

fn min_space_requirements(&self) -> (WidgetSizeRequirement, WidgetSizeRequirement) {
    (
        WidgetSizeRequirement::Fixed(unsafe {NonZeroU32::new_unchecked(200)}),
        WidgetSizeRequirement::Fixed(unsafe {NonZeroU32::new_unchecked(70)}),
    )
}

Let's specify what space requirements we want. We could use the space requirements of our children, but here let's just say we want a fixed size. when drawn, the app will give all the screen space to the root, and widgets will distribute that space to their children depending on their requirements and behaviour. For example, a sized box (or our counter button) request a fixed size, so when we will put this in a center widget, the center widget will receive the screen size, compare it with our own size, and give us the rect accordingly.

Finally, let's handle the events. We want to handle the button click, and increment the counter.

fn handle_event(&mut self, event: InputEvent, rect: softbuffer::Rect) -> EventResponse {

    let result = self.button.handle_event(event, rect);
    if result.contains(EventResponse::CALLBACK) {
        // in that case, increment the counnter and update the text.
        self.count += 1;
        self.text.set_text(self.count.to_string())
    }

    let result = result | EventResponse::REDRAW_REQUEST | !EventResponse::CALLBACK;

    result
}

If the button is pressed, it will return a callback flag. If we see this flag, we increment the counter and update the text. It is then important to return a draw request repsonse ourselves, to tell the app that we need to be redrawn. Here, we could simply return request redraw, but in more complex architectures, we might not now what events are thrown through our widgets. Also, it is worth noticing we removed the callback flag. This is left to the implementation type, but here as we consumed the event, I found it better to keep the information that our button triggered to ourselves.

Finally, let's implement a constructor for our counter.

pub fn new() -> Box<Counter> {
    Box::new(
        Counter {
            count: 0,
            button: Background::new(
                Color::rgb(200, 255, 200),
                Button::new(Empty::expand()),
            ),
            text: Text::new(
                "0".to_string(),
                TextStyle::default()
                    .sized(30.0)
                    .styled(FontStyle::Italic)
            ),
        }
    )
}

Here, simply create a new counter with an initial state, assign the button and a text. You will notice the button is behind a background. This is why I kept it under a dyn widget: it is not important to know what it is, as we won't interact with it. The events will be redirected from the background to the button, and the button will throw a callback when pressed that we will also receive through the background. You could think of this as a "background button". In Nerf, widgets won't assume any behaviour, and will only be used for their sole purpose. therfore, a button will only be used to throw callbacks, and a background will only be used to draw a background. If we want to have a background button, we will create a button, and add a background to it, as demonstrated here.

To avoid infinite size structs, most widgets are held in boxes in Nerf. It is however possible not to: our counter could have a straight text widget. But here, the app will expect a struct for the root, so our constructor returns a box.

Finally, let's create our app.

fn main() {
    let app = App::new(
        Align::new(
            Alignment::CENTER,
            Center::new(Counter::new()),
        )
    );

    // Run the app.
    app.run()
}

Here, simply create an app, put the counter as the root, and start it. You can see I added a center for convenience.

You might also like...
Yet Another Texture Packer - a small and simple CLI application to pack multiple textures/sprites into a texture atlas/sprite sheet

YATP (Yet Another Texture Packer) A small and simple CLI application to pack multiple textures/sprites into a texture atlas/sprite sheet. Installation

🚀 Yet another repository management with auto-attaching profiles.

🚀 ghr Yet another repository management with auto-attaching profiles. 🔥 Motivation ghq is the most famous solution to resolve stress of our reposito

Yet another phigros chart player.

prpr - P hig R os P layer, written in R ust Usage To begin with, clone the repo: git clone https://github.com/Mivik/prpr.git && cd prpr For compactnes

YAL is Yet Another scripting Language(but worse)

YAL - Yet Another Language YAL is yet another scripting language(but worse). Syntax Basic syntax fun main() { print("Hello, World!"); } Fibonacci

Yet another Python environment manager.

yen The easiest Python environment manager. Create virtual environments for any Python version, without needing Python pre-installed. Installation Get

A small unix and windows lib to search for executables in PATH folders.

A small unix and windows lib to search for executables in path folders.

A mirror of lib.rs/main

Lib.rs (Crates.rs) Lib.rs is a fast, lightweight index of Rust libraries and applications. Crates published to crates.io will automatically show up on

Speech-to-text lib for Melba Toast

Melba-stt A rust discord bot that joins a voice channel and transcribes spoken audio from each user. Running Install the rust toolchain With CUDA and

Simple, cross-platform GameMaker lib for getting file metadata

File Metadata Tiny baby library for getting file metadata. Originally written to work for a GameMaker game a friend is creating. Table of Contents Ins

Owner
Eclipse
Computer science student - projects incoming !
Eclipse
Yet another sort crate, porting Golang sort package to Rust.

IndexSort IndexSort Yet another sort crate (in place), porting Golang's standard sort package to Rust. Installation [dependencies] indexsort = "0.1.0"

Al Liu 4 Sep 28, 2022
⚡🦀 Yet another rust system info fetcher.

Yarsi: Yet another rust sys info fetcher ✨ Showcase requirements ?? cargo ?? install with $ curl https://sh.rustup.rs -sSf | sh installation ❤️‍?? Ya

BinaryBrainiacs 8 Jan 26, 2023
Yet another command-line chat GPT frontend written in Rust.

gpterm Yet another command-line chat GPT frontend written in Rust. Features Stream output with typing effect Store chat messages/history Context aware

Makis Christou 22 May 4, 2023
Yet another code execution engine written in Rust.

exec Yet another blazingly fast code execution engine written in Rust. Paths GET /api/v1/status GET /api/v1/runtimes POST /api/v1/execute POST /api/v1

Stefan Asandei 2 Jul 11, 2023
Yay - Yet another Yogurt - An AUR Helper written in Go

Yay Yet Another Yogurt - An AUR Helper Written in Go Help translate yay: Transifex Features Advanced dependency solving PKGBUILD downloading from ABS

J Guerreiro 8.6k Jan 1, 2023
yet another typing test, but crab flavoured

toipe A trusty terminal typing tester for the tux. Usage Install cargo install toipe Run typing test toipe looks best on a nice terminal (such as Ala

Samyak Sarnayak 431 Dec 20, 2022
Yet another fractal generator (based on glium)

Juliabrot Yet another fractal generator. Juliabrot is a Rust application using the OpenGL Framework to render in realtime. Install Rust To download Ru

Max 2 Feb 27, 2022
Yet another Codeforces cli

cf-tool-rs A Rust implement for https://github.com/xalanq/cf-tool WIP. Pull Requests / Contributions are welcomed! How to Configure? Configure File sh

Woshiluo Luo 2 May 8, 2022
Yet Another Kalman Filter Implementation. As well as Lie Theory (Lie group and algebra) on SE(3). [no_std] is supported by default.

yakf - Yet Another Kalman Filter Yet Another Kalman Filter Implementation, as well as, Lie Theory (Lie group, algebra, vector) on SO(3), SE(3), SO(2),

null 7 Dec 1, 2022
Yet another lightweight and easy to use HTTP(S) server

Raptor Web server Raptor is a HTTP server written in Rust with aims to use as little memory as possible and an easy configuration. It is built on top

Volham 5 Oct 15, 2022