1 unstable release

0.3.0 Feb 13, 2024

#138 in Cargo plugins

Apache-2.0

2KB

Finestra

CI Crates.io Version GitHub License

Finestra is a simple and practical desktop UI framework for Rust. It maintains the authentic look and feel of each platform by integrating with their native UI backends. With Finestra, you can write an application that targets both Windows and macOS.

Installation

Finestra provides a crate which contains all the tools you need to start developing desktop applications:

[dependencies]
finestra = "0.1.0"

Example

The following example demonstrates the basic usage of Finestra, by providing a button that get its text updated each time it is clicked.

use finestra::*;

struct MyApplication;

impl AppDelegate<AppState> for Application {
    fn make_content_view(&mut self, state: &mut AppState, _: Window) -> impl finestra::View<Self, AppState>  {
        state.label.set("Count: 0");

        Button::new(&state.label)
            .with_on_click(|state: &mut AppState, window: Window| {
                state.count += 1;
                state.label.set(format!("Count: {}", state.count));

                if state.count % 10 == 0 {
                    window.create_dialog(format!("You clicked {} times!", state.count))
                        .show();
                }
            })
    }
}

#[derive(Debug, Default)]
struct AppState {
    count: usize,
    label: TextValue,
}

fn main() {
    App::with_state(MyApplication, AppState::default())
        .run();
}

Usage

The crate provides a single entrypoint, the App structure.

App::new(MyApplication::default()).run()

To react to common events (such as launching) and to configure and populate the window, you must provide an AppDelegate implementation.

struct MyApplication;

impl AppDelegate for MyApplication {
    fn did_launch(&mut self, _: &mut ()) {
        println!("Taking Off 🚀");
    }

    fn configure_main_window(&mut self, _: &mut ()) -> WindowConfiguration {
        WindowConfiguration::new()
            .with_title("Exciting Window Title 🤩")
    }

    fn will_show_window(&mut self, _: Window, _: &mut ()) {
        println!("About to show the window, be prepared! 👀");
    }

    fn make_content_view(&mut self, _: &mut (), _: Window) -> impl View<Self> {
        Label::new("Welcome to Finestra!")
    }
}

State

A powerful tool for application development is the State<T> object, which is a shared and subscribed object that you can use to write once, update everywhere. It is akin to WPF's Binding and SwiftUI's Binding. It is also deeply integrated in the library, for example by allowing you to pass a State<String> everywhere you pass a String/&str.

In the following example, the user can modify the title of the window by changing the contents of a TextField:

struct Application;

impl AppDelegate<AppState> for Application {
    fn configure_main_window(&mut self, state: &mut AppState) -> WindowConfiguration {
        state.title.set("My Application");

        WindowConfiguration::new()
            .with_title(state.title.clone())
    }

    fn make_content_view(&mut self, state: &mut AppState, _: Window) -> impl finestra::View<Self, AppState>  {
        Stack::horizontal()
            .with(Label::new("Choose a title:"))
            .with(TextField::new(state.title.clone()))
    }
}

#[derive(Debug, Default)]
struct AppState {
    title: TextValue,
}

fn main() {
    App::with_state(Application, AppState::default())
        .run();
}

Click here for the full example.

Stacking

To place multiple items in the same row or column, use the Stack view. This can be horizontal (row-like) or vertical (column-like).

fn make_content_view(&mut self, _: &mut (), _: Window) -> impl finestra::View<Self, ()> {
    Stack::horizontal()
        .with(Label::new("Hello, world!"))
        .with(Button::new("Click Me"))
        .with(Button::new("Goodbye, world!"))
}

To see how these can be combined to create a powerful interface, see the Calculator App example.

Dialogs

When a specific event occurs that requires attention from the user, you can use dialog boxes.

use rand::seq::SliceRandom;

const FRUITS: &[&str] = &[
    "Apple", "Banana",
    "Strawberry", "Orange",
    "Kiwi", "Pear",
    "Berries", "Lemon",
    // "Tomato",
];

Button::new("Random Fruit")
    .with_on_click(|_, window| {
        window.create_dialog(FRUITS.choose(&mut rand::thread_rng()))
                .title("Fruit")
                .kind(DialogKind::Informational)
                .show();
    })

For further information, consult the documentation of DialogBuilder and DialogKind.

Colors

To use colors that are consistent with every platform Finestra supports, you can use the SystemColor enumeration:

Label::new("BlueExampleSoftware")
    .with_color(Color::system(SystemColor::Blue))

These will ensure that you always use the correct color, harmonizing with the system applications.

If you need to use a specific color, you can of use the Color::rgb() and Color::rgba() functions:

Label::new("Maroon Balloon 🎈")
    .with_color(Color::rgb(155, 68, 68))

You can naturally use the State<Color> pattern for changing colors dynamically. See the Disco Button example to see how it's implemented.

Component Overview

The following components are supported by Finestra at the moment:

  • Button can be used to invoke a specific action.
  • ImageView can display images.
  • Label contains a single line of text.
  • Stack places items horizontally or vertically.
  • TextBlock can contain multiple lines of text, and allows for specific alignment.
  • TextField can be used to request a specific string from the user.

Rationale

Operating Systems often specify their own design language, e.g. Apple's Human Interface Guidelines and Microsoft's Windows 11 Design Principles. These guidelines are provided to let users experience a consistent and familiar user interface, and honoring them is almost always appreciated by the users of your applications, just like Arc for Windows was praised on X/Twitter.

_TODO: Update with blogpost_

Further Reading

Copyright (C) 2024 Tristan Gerritsen

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Serde by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

No runtime deps