Giorgio Pomettini (@pomettini)
- Why using
libui
as a cross-platform GUI library - How to use the
libui-rs
library - Have a look at every UI control
- Write an example project
- How to test a GUI application
- What is currently missing
QT
is great but...- If you want to statically link the DLL you have to get a license
- Or you have to bundle your app with a
QT
installer - That was not the case for me, I wanted simple, portable app
- What about
GTK
? - Sure
GTK
is great but... - It looks terrible on Windows and macOS :(
- What about
azul
or other Electron-based libraries? - Sure they works fine, but I really wanted to make a native app
- (I'm not against Electron by the way, I think it's great!)
- Currently is a bit in early stage at the moment
- Have a look at https://areweguiyet.com/ for reference
- Also: https://users.rust-lang.org/t/current-state-of-gui-development-in-rust/
iui
is a simple (about 4 kLOC of Rust)- Small (about 800kb, including
libui
) - Easy to distribute (one shared library)
- Provides a Rusty user interface library that binds to native APIs
iui
wraps native retained mode GUI libraries:- Win32API on Windows
- Cocoa on macOS
- GTK+ on Linux
Add iui
to your project with:
iui = { git = "https://github.com/rust-native-ui/libui-rs.git" }
I advise you to get it from the master
branch as the current version on crates.io is missing some of the latest changes
Yeah it works fine!
- Button
- Checkbox
- Combobox
- Entry
- Label
- MultilineEntry
- Slider
- Spinbox
- Group
- HorizontalBox
- HorizontalSeparator
- LayoutGrid
- Spacer
- TabGroup
- VerticalBox
extern crate iui;
use iui::controls::*;
use iui::prelude::*;
fn main() {
let ui = UI::init().unwrap();
let mut win = Window::new(&ui, "Hello Rust Rome", 200, 200, WindowType::NoMenubar);
let label = Label::new(&ui, "Hello world!");
win.set_child(&ui, label);
win.show(&ui);
ui.main();
}
- Init the UI
- Create a new window with a UI reference, the title and the size
- Create a label control with a UI reference and the text
- Set the label as the child of window
- Call
window.show()
- Call
ui.main()
fn main() {
let ui = UI::init().unwrap();
let mut win = Window::new(&ui, "Hello Rust Rome", 200, 200, WindowType::NoMenubar);
let mut vbox = VerticalBox::new(&ui);
let mut label = Label::new(&ui, "I'm a Label");
vbox.append(&ui, label, LayoutStrategy::Compact);
let mut button = Button::new(&ui, "I'm a Button");
vbox.append(&ui, button, LayoutStrategy::Compact);
let mut combobox = Combobox::new(&ui);
combobox.append(&ui, "I'm a Combobox");
combobox.set_selected(&ui, 0);
vbox.append(&ui, combobox, LayoutStrategy::Compact);
win.set_child(&ui, vbox);
win.show(&ui);
ui.main();
}
You can set a padding on a vbox
using
vbox.set_padded(&ui, true);
- Compact: Uses the minimum possible size to contain its content
- Stretchy: Make the control expand to its maximum size
This is an example with all the controls set to Stretchy:
fn main() {
...
let mut entry = Entry::new(&ui);
vbox.append(&ui, entry, LayoutStrategy::Compact);
let mut m_entry = MultilineEntry::new(&ui);
vbox.append(&ui, m_entry, LayoutStrategy::Compact);
let mut slider = Slider::new(&ui, 0, 10);
vbox.append(&ui, slider, LayoutStrategy::Compact);
let mut separator = HorizontalSeparator::new(&ui);
vbox.append(&ui, separator, LayoutStrategy::Compact);
let mut spinbox = Spinbox::new(&ui, 0, 10);
vbox.append(&ui, spinbox, LayoutStrategy::Compact);
...
}
You can add controls inside VerticalBox
, HorizontalBox
, Group
and TabGroup
like if they are matrioskas
fn main() {
...
let mut hbox = HorizontalBox::new(&ui);
hbox.set_padded(&ui, true);
let mut group_left = Group::new(&ui, "Left Group");
hbox.append(&ui, group_left.clone(), LayoutStrategy::Stretchy);
let mut label = Label::new(&ui, "Left Label");
group_left.set_child(&ui, label);
let mut group_right = Group::new(&ui, "Right Group");
hbox.append(&ui, group_right.clone(), LayoutStrategy::Stretchy);
let mut entry = MultilineEntry::new(&ui);
group_right.set_child(&ui, entry);
...
}
fn main() {
...
let mut button = Button::new(&ui, "I'm a Button");
vbox.append(&ui, button.clone(), LayoutStrategy::Compact);
button.on_clicked(&ui, {
let ui = ui.clone();
move |btn| {
btn.set_text(&ui, "I have been clicked!");
}
});
...
}
We need to clone the button
in order to pass the pointer to the vbox
and the closure
fn main() {
...
let mut label = Label::new(&ui, "0");
vbox.append(&ui, label.clone(), LayoutStrategy::Compact);
let mut button = Button::new(&ui, "Increase");
vbox.append(&ui, button.clone(), LayoutStrategy::Compact);
button.on_clicked(&ui, {
let ui = ui.clone();
let mut label = label.clone();
move |_| {
// Get the value from the label
let mut counter: i32 = label.text(&ui).parse().unwrap();
// Increase the value
counter += 1;
// Put it back to the label
label.set_text(&ui, &counter.to_string());
}
});
...
}
fn main() {
...
let mut button = Button::new(&ui, "Open Modal");
button.on_clicked(&ui, {
let ui = ui.clone();
let win = win.clone();
move |_| {
// Message window
win.modal_msg(&ui, "This is the title", "This is a message");
// Error window
win.modal_err(&ui, "This is the title", "This is an error");
}
});
...
}
fn main() {
...
let mut button = Button::new(&ui, "Open Modal");
button.on_clicked(&ui, {
let ui = ui.clone();
let win = win.clone();
move |_| {
// Returns an Option<PathBuf>
let file = win.open_file(&ui);
// Match the result (will be None if user closes the window)
match file {
Some(file_path) => println!("{:?}", file_path),
None => println!("No file selected"),
}
}
});
...
}
Sometimes you may have a lot of states you want to move between controls, in that case you use a struct to hold all the values
struct State {
slider_val: i32,
entry_val: String,
}
And you can use the Interior Mutability Pattern to move them around your application
let state = Rc::new(RefCell::new(State {
slider_val: 0,
entry_val: "".into(),
}));
This way you don't have to clone all your controls every time
slider.on_changed(&ui, {
let state = state.clone();
move |val| {
state.borrow_mut().slider_val = val;
}
});
entry.on_changed(&ui, {
let state = state.clone();
move |val| {
state.borrow_mut().entry_val = val;
}
});
We don't need to create or open a window
, so we'll just need to init the ui
It should be easy, right?
#[test]
fn test_change_label() {
let ui = UI::init().unwrap();
let mut label = Label::new(&ui, "");
label.set_text(&ui, "Hello Rome");
assert_eq!(label.text(&ui), "Hello Rome");
}
Nope.
You have a bug: Some data was leaked;
either you left a uiControl lying around or there's a bug in libui itself. Leaked data:
0x7fca1941ebd0 uiLabel
error: process didn't exit successfully: (signal: 4, SIGILL: illegal instruction)
The C library will detect memory leaks and kill the process if there is a control lying around
So you have to call free manually
I wrote a convenient macro that calls destroy on any control
you will pass to it
macro_rules! FREE {
($control:expr) => {
unsafe {
// Memory needs to be released, otherwise it will panic
Into::<Control>::into($control).destroy();
}
};
}
#[test]
fn test_change_label() {
let ui = UI::init().unwrap();
let mut label = Label::new(&ui, "");
label.set_text(&ui, "Hello Rome");
let value = label.text(&ui);
FREE!(label);
assert_eq!(value, "Hello Rome");
}
Now call cargo test
test test_change_label ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Warning: The GUI stuff runs on the main thread so be sure to turn off tests on multiple threads
RUST_TEST_THREADS=1 cargo test
- Trees
- Clipboard support
- Drag and drop support
- OpenGL support
libui
documentation is nonexistent- Static linking of
libui
Absolutely yes for your side-projects
For other kind of projects, I'd wait a little bit more :)
Copyright © 2019 Giorgio Pomettini This software released under the MIT License.