I've been doing some web programming using preact these last weeks and one of the things that impressed the most was the way logic and UI are split in code. When doing functional components this is specially explicit. Your function returns whatever should be displayed by the component and receives a props
object containing all the information the components need to render. A component rerenders when some of its props
are changed, either because an external component made changes or in response to an event.
IMHO, this really helps to organize code and separate concerns. All the logic is written in terms of the components' properties. The GUI is updated in response to changes in the properties in order to synchronize the component state with what is shown on screen.
We can also achieve this in Vala using properties
and bindings
from GLib. In this article we will produce a login screen that validates the username and password fields. All code is available at Github.
Important: The repo contains many iterations of the code presented in this article. A comment containing the name of the original file is shown at the top of each code listing.
Let's start with the basic code to build the form above. This is just standard GTK code: all the ui elements are created in the construct
method. The original file (v1/main.vala) also contains a Gtk.Application
subclass that handles initialization and showing our form.
Vala tip: the construct
block runs during an object's creation and after named constructors are called.
// excerpt from v1.vala
class LoginForm : Gtk.Grid {
// ......
construct {
width_request = 300;
margin = 10;
column_homogeneous = false;
expand = false;
valign = Gtk.Align.CENTER;
halign = Gtk.Align.CENTER;
var username_field = new Gtk.Entry () {
hexpand = true,
margin = 4
};
var password_field = new Gtk.Entry () {
hexpand = true,
margin = 4
};
var validation_warning = new Gtk.Label ("") {
wrap = true,
height_request = 50
};
var login_btn = new Gtk.Button.with_label ("Login") {
expand = false,
halign = Gtk.Align.END
};
attach (new Gtk.Label ("Username"), 0, 0);
attach (username_field, 1, 0);
attach (password_field, 1, 1);
attach (new Gtk.Label ("Password"), 0, 1);
attach (validation_warning, 1, 2);
attach (login_btn, 1, 3);
show_all ();
}
}
There's no interactivity in this form yet. In fact, it already starts in an inconsistent state: the Login
button is enabled even though block fields are empty. We could fix this simply by setting sensitive=false
in login_btn
initialization, but we won't. Instead we will create a property called is_valid
and bind its value to login_btn.sensitive
. Every time one of the values change the other is instantly updated. Bindings can also be created with many values as long as a cicle is not introduced.
Let's use this now in our program: first we create a property is_valid
and then we use bind_property to bind its value to login_btn.sensitive
. Our binding flags indicate that the binding is bidirectional and that the value is synchronized when the binding is created.
// excerpt from v2.vala
class LoginForm : Gtk.Grid {
public bool is_valid { get; set; default = false; }
construct {
// GUI creation code
bind_property ("is_valid", login_btn, "sensitive", BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
}
Now the login button is disabled when the app starts and will remain so until we set is_valid=true
. Unfortunately this never happens, so let's add a property username
and bind it to username_field.text
. When username
is set we check if its length is larger than 0 and set is_valid
accordingly.
// excerpt from v3.vala
class LoginForm : Gtk.Grid {
public bool is_valid { get; set; default = false; }
private string _username = "";
public string username { get {
return _username;
}
set {
_username = value;
if (value.length > 0) {
is_valid = true;
}
else {
is_valid = false;
}
}}
construct {
// GUI creation code
bind_property ("is_valid", login_btn, "sensitive", BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
bind_property ("username", username_field, "text", BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
}
Run the app and test the new validation: the login button should be disable when the app starts and respond to is_valid
. IMHO, the impressive part of this code is that all GUI code is on construct
and our logic is written only in terms of properties. When we update the Widget's properties the UI will update in response, so our application state is always consistent.
Now the final step is to set validation_error
to a meaningful message. We can do this with another property binding. To make code more readble we also refactor all our validation code into a function void validate_form ()
.
// excerpt from v4.vala
class LoginForm : Gtk.Grid {
public string validation_error { get; set; default = ""; }
public string username { get {
return _username;
}
set {
_username = value;
validate_form ();
} }
//....
construct {
// ....
bind_property ("validation_error", validation_warning, "label", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE);
}
private void validate_form () {
if (_username.length == 0) {
is_valid = false;
validation_error = "Empty username";
return;
}
is_valid = true;
validation_error = "";
}
And that's it for the username
field. We can do exactly the same for the password
field and arrive at the final code available at final.vala
in the example repository.
Another interesting project is Blueprint compiler, which creates a sort of DSL for Gtk components. So, what do you think about this style of Gtk programming? Join the conversation in the comments ;)
Top comments (0)