[−][src]Crate vgtk
A declarative UI framework built on GTK and Gtk-rs.
Overview
vgtk
is a GUI framework built on GTK using what might be
called the "Model-View-Update" pattern, as popularised in Elm
and Redux, in addition to a component model similar to React.
Its primary inspiration is the Yew web framework for Rust, from
which it inherits most of its more specific ideas.
To facilitate writing GTK UIs in a declarative style, vgtk
implements
an algorithm similar to DOM diffing, but for GTK's widget tree, which
has turned out to be considerably less trivial than diffing a well structured
tree like the DOM, but as a first draft at least it gets the job done.
More importantly, vgtk
also provides the gtk!
macro
allowing you to write your declarative UI in a syntax very similar to JSX.
Show Me!
use vgtk::{ext::*, gtk, run, Component, UpdateAction, VNode}; use vgtk::lib::{gtk::*, gio::ApplicationFlags}; #[derive(Clone, Default, Debug)] struct Model { counter: usize, } #[derive(Clone, Debug)] enum Message { Inc, Exit, } impl Component for Model { type Message = Message; type Properties = (); fn update(&mut self, message: Message) -> UpdateAction<Self> { match message { Message::Inc => { self.counter += 1; UpdateAction::Render } Message::Exit => { vgtk::quit(); UpdateAction::None } } } fn view(&self) -> VNode<Model> { gtk! { <Application::new_unwrap(None, ApplicationFlags::empty())> <Window border_width=20 on destroy=|_| Message::Exit> <HeaderBar title="inc!" show_close_button=true /> <Box spacing=10 halign=Align::Center> <Label label=self.counter.to_string() /> <Button label="inc!" image="add" always_show_image=true on clicked=|_| Message::Inc /> </Box> </Window> </Application> } } } fn main() { std::process::exit(run::<Model>()); }
Prerequisites
The vgtk
documentation assumes you already have a passing familiarity with GTK and
its Rust bindings. It makes little to no effort to explain how GTK works or
to catalogue which widgets are available. Please refer to the Gtk-rs documentation or
that of GTK proper for this.
The Component Model
The core idea of vgtk
is the Component
. A component, in practical terms, is a
composable tree of Gtk widgets, often a window, reflecting a block of application state. You
can write your application as a single component, but you can also embed a component inside
another component, which makes sense for parts of your UI you tend to repeat, or just for
making an easier to use interface for a common Gtk widget type.
Your application starts with a component that manages an Application
object.
This Application
in turn will have one or more Window
s attached
to it, either directly inside the component or as subcomponents. Window
s in turn
contain widget trees.
You can think of a component as an MVC system, if that's something you're familiar with: it contains some application state (the Model), a method for rendering that state into a tree of GTK widgets (the View) and a method for updating that state based on external inputs like user interaction (the Controller). You can also think of it as mapping almost directly to a React component, if you're more familiar with that, even down to the way it interacts with the JSX syntax.
Building A Component
A component in vgtk
is something which implements the Component
trait,
providing the two crucial methods view
and update
.
Your top level component should have a view
function which returns
a GTK Application
object, or, rather, a "virtual DOM" tree which builds one.
The view
function's job is to examine the current state of the component
(usually contained within the type of the Component
itself) and return a UI tree
which reflects it. This is its only job, and however much you might be tempted to, it must not do
anything else, especially anything that might block the thread or cause a delayed result.
Responding to user interaction, or other external inputs, is the job of the
update
function. This takes an argument of the type
Component::Message
and updates the component's state according to the
contents of the message. This is the only place you're allowed to modify the contents of your
component, and every way to change it should be expressed as a message you can send to
your update
function.
update
returns an UpdateAction
describing one of three
outcomes: either, None
, meaning nothing significant changed as a result
of the message and we don't need to update the UI, or Render
, meaning
you made a change which should be reflected in the UI, causing the framework to call your
view
method and re-render the UI. Finally, you can also return
Defer
with a Future
in case you need to
do some I/O or a similar asynchronous task - the Future
should resolve to a
Component::Message
which will be passed along to update
when the Future
resolves.
Signal Handlers
Other than UpdateAction::Defer
, where do these messages come from?
Usually, they will be triggered by user interaction with the UI. Using the gtk!
macro, you can attach signal handlers to
GTK signals
which respond to a signal by sending a message to the current component.
For instance, a GTK Button
has a clicked
signal which is
triggered when the user clicks on the button, as the name suggests. Looking at the
connect_clicked
method, we see that it takes a single &Self
argument,
representing the button being clicked. In order to listen to this signal, we attach a closure
with a similar function signature to the button using the on
syntax. The closure always takes the
same arguments as the connect_*
callback, but instead of returning nothing it returns a message of
the component's message type. This message will be passed to the component's
update
method by the framework.
gtk! { <Button label="Click me" on clicked=|_| Message::ButtonWasClicked /> }
This will cause a Message::ButtonWasClicked
message to be sent to your component's
update
function when the user clicks the button.
Signal handlers can also be declared as async
, which will cause the framework to wrap the handler
in an async {}
block and await
the
message result before passing it on to your update function. For instance, this very contrived
example shows a message dialog asking the user to confirm clicking the button before sending the
ButtonWasClicked
message.
gtk! { <Button label="Click me" on clicked=async |_| { vgtk::message_dialog( vgtk::current_window().as_ref(), DialogFlags::MODAL, MessageType::Info, ButtonsType::Ok, true, "Please confirm that you clicked the button." ).await; Message::ButtonWasClicked } /> }
The gtk!
Syntax
The syntax for the gtk!
macro is similar to JSX, but with a number of necessary
extensions.
A GTK widget (or, in fact, any GLib object, but most objects require widget children) can be
constructed using an element tag. Attributes on that tag correspond to get_*
and set_*
methods
on the GTK widget. Thus, to construct a GTK Button
calling set_label
to set its label:
gtk! { <Button label="Click me" /> }
A GTK container is represented by an open/close element tag, with child tags representing its children.
gtk! { <Box orientation=Orientation::Horizontal> <Button label="Left click" /> <Button label="Right click" /> </Box> }
If a widget has a constructor that takes arguments, you can use that constructor in place
of the element's tag name. This syntax should only be used in cases where a widget simply cannot be constructed
using properties alone, because the differ isn't able to update arguments that may have changed
in constructors once the widget has been instantiated. It should be reserved only for when it's
absolutely necessary, such as when constructing an Application
, which doesn't
implement Buildable
and therefore can't be constructed in any way other than through
a constructor method.
gtk! { <Application::new_unwrap(None, ApplicationFlags::empty()) /> }
Sometimes, a widget has a property which must be set through its parent, such as a child's
expand
and fill
properties inside a Box
. These properties correspond to
set_child_*
and get_child_*
methods on the parent, and are represented as attributes
on the child with the parent's type as a namespace, like this:
gtk! { <Box> <Button label="Click me" Box::expand=true Box::fill=true /> </Box> }
The final addition to the attribute syntax pertains to when you need to qualify an
ambiguous method name. For instance, a MenuButton
implements both
WidgetExt
and MenuButtonExt
, both of which contains
a set_direction
method. In order to let the compiler know which one you mean, you
can qualify it with an @
and the type name, like this:
<MenuButton @MenuButtonExt::direction=ArrowType::Down /> <MenuButton @WidgetExt::direction=TextDirection::Ltr />
Interpolation
The gtk!
macro's parser tries to be smart about recognising Rust expressions as attribute
values, but it's not perfect. If the parser chokes on some particularly complicated Rust
expression, you can always wrap an attribute's value in a {}
block, as per JSX.
This curly bracket syntax is also used to dynamically insert child widgets into a tree. You can insert a code block in place of a child widget, which should return an iterator of widgets that will be appended by the macro when rendering the virtual tree.
For instance, to dynamically generate a series of buttons, you can do this:
gtk! { <Box> { (1..=5).map(|counter| { gtk! { <Button label=format!("Button #{}", counter) /> } }) } </Box> }
Subcomponents
Components are designed to be composable, so you can place one component inside
another. The gtk!
syntax for that looks like this:
<@Subcomponent attribute_1="hello" attribute_2=1337 />
The subcomponent name (prefixed by @
to distinguish it from a GTK object) maps to
the type of the component, and each attribute maps directly to a property on its
Component::Properties
type. When a subcomponent is constructed,
the framework calls its create
method with the property object constructed
from its attributes as an argument.
A subcomponent needs to implement create
and change
in addition to update
and view
. The default implementations
of these methods will panic with a message telling you to implement them.
Subcomponents do not support signal handlers, because a component is not a GTK object. You'll have
to use the Callback
type to communicate between a subcomponent and its parent.
This is what a very simple button subcomponent might look like:
#[derive(Clone, Debug, Default)] pub struct MyButton { pub label: String, pub on_clicked: Callback<()>, } #[derive(Clone, Debug)] pub enum MyButtonMessage { Clicked } impl Component for MyButton { type Message = MyButtonMessage; type Properties = Self; fn create(props: Self) -> Self { props } fn change(&mut self, props: Self) -> UpdateAction<Self> { *self = props; UpdateAction::Render } fn update(&mut self, msg: Self::Message) -> UpdateAction<Self> { match msg { MyButtonMessage::Clicked => { self.on_clicked.send(()); } } UpdateAction::None } fn view(&self) -> VNode<Self> { gtk! { <Button label=self.label.clone() on clicked=|_| MyButtonMessage::Clicked /> } } }
Note that because this component doesn't have any state other than its properties, we
just make Self::Properties
equal to Self
, there's no need to keep two identical types
around for this purpose. Note also that the callback passes a value of type ()
, because
the clicked
signal doesn't contain any useful information besides the fact that it's
being sent.
This is how you'd use this subcomponent with a callback inside the view
method of a parent component:
fn view(&self) -> VNode<Self> { gtk! { <Box> <Label label="Here is a button:" /> <@MyButton label="Click me!" on clicked=|_| ParentMessage::ButtonClicked /> </Box> } }
Note that the return type of the on_clicked
callback is the message type of the parent
component - when the subcomponent is constructed, the parent component will wire any callback
up to its update
function for you automatically with a bit of unsafe
trickery, so that the subcomponent doesn't have to carry the information about what type of
parent component it lives within inside its type signature. It'll just work, with nary a
profunctor in sight.
Logging
vgtk
uses the log
crate for debug output. You'll need to provide your own logger for this;
the example projects show how to set up pretty_env_logger
for logging to the
standard output. To enable it, set the RUST_LOG
environment variable to debug
when running the
examples. You can also use the value vgtk=debug
to turn on debug output only for vgtk
, if you have
other components using the logging framework. At log level debug
, it will log the component messages
received by your components, which can be extremely helpful when trying to track down a bug
in your component's interactions. At log level trace
, you'll also get a lot of vgtk
internal
information that's likely only useful if you're debugging the framework.
Work In Progress
While this framework is currently sufficiently usable that we can implement TodoMVC in it, there
are likely to be a lot of rough edges still to be uncovered. In particular, a lot of properties on
GTK objects don't map cleanly to get_*
and set_*
methods in the Gtk-rs mappings, as required
by the gtk!
macro, which has necessitated the collection of hacks in
vgtk::ext
. There are likely many more to be found in widgets as yet unused.
As alluded to previously, the diffing algorithm is also complicated by the irregular structure of the
GTK widget tree. Not all child widgets are added through the Container
API, and while
most of the exceptions are already implemented, there will be more. There's also a lot of room yet
for optimisation in the diffing algorithm itself, which is currently not nearly as clever as the state
of the art in the DOM diffing world.
Not to mention the documentation effort.
In short, pull requests are welcome.
Modules
ext | Helper traits for adapting the GTK API to the |
lib | Re-exports of GTK and its associated libraries. |
types | Useful types for GTK extensions. |
Macros
gtk | Generate a virtual component tree. |
gtk_if | Generate a virtual component tree only if a condition is true. |
on_signal | Connect a GLib signal to a |
stream_signal | Connect a GLib signal to a |
Structs
Callback | A callback property for sub- |
MenuBuilder | Makes a |
Scope | A channel for sending messages to a |
VNodeIterator | An iterator over zero or one |
Enums
UpdateAction | An action resulting from a |
VNode | A node in the virtual component tree representing a |
Traits
Component | This is the trait your UI components should implement. |
Functions
current_object | Get the current |
current_window | Get the current |
menu | Construct a |
message_dialog | Open a simple |
quit | Tell the running |
run | Run an |
run_dialog | Launch a |
run_dialog_props | Launch a |
start | Start an |