Drawing in GTK in Rust (part 1)
--
It’s a double challenge: I’m learning Rust (but I can say I more or less know the language by now), and I’m trying to use GTK in Rust. I’m a complete beginner in GTK, and even if I know Rust, it does not mean I can use it properly. So I’ll learn a huge library (written in C) by using it in a complex and hard language I just learned. Well, well, good luck, me.
(Side note: I hadn’t forgot about Rust type language, second part is in drafts but I need time to process that stuff to provide a consistent reading).
Our goals:
- Create a resizable window
- Have a drawing area in this window
- Draw something there in realtime
- Have a some dropbox element to change something (color? Good idea)
- React to Enter by copying content of the drawing area to the clipboard (this part I’ve solved earlier).
The list looks reasonable, and I shouldn’t have any troubles to do this if only I know how to it. That’s the main curse of lore. You understand what to do, but you don’t know conventions on how to press buttons there, and how they call those buttons there…
Anyway, let’s go.
Resizable window
I already have a stub for creating a window: [1]
The main difference from a reference is that I moved app into a normal function instead of closure.
By sheer luck it’s resizable, but it’s not my achievement.
Lesson learned: If I skip gio::prelude, Bad Things happens:
the method `connect_activate` exists but the following trait bounds were not satisfied:
`gtk::Application : gtk::ButtonExt`
`gtk::Application : gtk::EntryExt`
`gtk::Application : gtk::ExpanderExt`
`gtk::Application : gtk::FlowBoxChildExt`
`gtk::Application : gtk::GtkMenuItemExt`
`gtk::Application : gtk::ListBoxRowExt`
`gtk::Application : gtk::StatusIconExt`
`gtk::Application : gtk::SwitchExt`
Huh. If I hadn’t know that I need to use prelude for gio
, it would have been an hour long debugging. (BTW: I have no idea what is gio
and why it’s not in gtk
…)
How others do this?
With help of this SO comment, I’ll look closely into process-viewer. They can do this using GTK:
(picture here).
The actual code to draw graph is here. Notes to myself:
- use cairo;
- use gtk::{self, BoxExt, ContainerExt, DrawingArea, ScrolledWindowExt, StateFlags, WidgetExt};
- use gdk::{self, WindowExt};
… and here my first lore question: how it calls GTK for start? My first guess was a Connecter
trait, but it’s declared in the code, therefore it should not be used by underlying libraries. The second thing we have Graph
structure with corresponding impl
. No clue. My next guess is that something at higher level is calling us. The Connecter
trait looks as the most prominent candidate.
Before reading it out, let’s confirm it. Bingo, Connecter is found twice — in the file above and here.
Yep, here the curated except from this file:
cpu_usage_history: Rc<RefCell<Graph>>let mut cpu_usage_history = Graph::new(Some(100.), false);
cpu_usage_history.set_label_callbacks(...)
cpu_usage_history.attach_to(&vertical_layout);
let cpu_usage_history = connect_graph(cpu_usage_history); # FUNKY!
...cpu_usage_history.connect_to_window_events();
All this happens in create_process_dialog
function, which is called once from create_new_proc_diag
function here, which are used in the same file, inside build_ui
function, which is called from the main
. Bingo! We’ve reconstructed a call stack. Now we need to extract a minimal drawing protocode from this.
application.connect_startup(move |app| { build_ui(app); });...
info_button.connect_clicked(...
...sys => move |_| {...create_new_proc_diag(&process_dialogs, pid, &*sys.borrow(), &window, &*running_since.borrow(),start_time);}
...#inside create_new_proc_diaglet mut notebook = NoteBook::new();
let mut ram_usage_history = Graph::new(Some(total_memory as f64), true);
vertical_layout.add(>k::Label::new(Some("Memory usage")));
let ram_usage_history = connect_graph(ram_usage_history);
ram_usage_history.attach_to(&vertical_layout);scroll.connect_show(clone!(ram_usage_history, cpu_usage_history => move |_| {
cpu_usage_history.borrow().show_all(); }));notebook.create_tab("Resources usage", &scroll);
let area = popup.get_content_area();
area.pack_start(¬ebook.notebook, true, true, 0);
popup.get_preferred_width();
ram_usage_history.connect_to_window_events();
return ProcDialog {...ram_usage_history,...}
...
Funny enough, nothing true happens inside connect_to_window_events
. The actual drawing happens inside draw
function for Graph
trait
Insofar I got a lot of words here: notebook
, area
, scroll
,… Now I’ll try to use those in my app. But who calls draw
?
pub fn connect_graph(graph: Graph) -> Rc<RefCell<Graph>> {
let area = graph.area.clone();
let graph = Rc::new(RefCell::new(graph));
area.connect_draw(clone!(graph => move |w, c| {
graph.borrow()
.draw(...}));
graph
}
It’s heavily related to this line from previous snippet:
let ram_usage_history = connect_graph(ram_usage_history);
As I can see, there are a lot of closures in GTK, we attach them to stuff. I’m not completely understand how closures can interact with the rest of application (but I’ll find out this soon).
Docs intermission
Before trying to combine those lines into my own proof-of-concept code I’d like to procrastinate a bit over GTK documentation for those keywords.
Oh, by the way, there are tons of books on GTK, just to help to continue to procrastinate for as long as needed.
Nah, I want to keep it short.
Notebook (GTK Notebook) — A tabbed notebook container:
High chance I don’t need it in my simple case. But it’s good to know that it exists and to know it’s name.
Both ‘area’ and ‘scroll’ puzzle me, as I can’t find direct pages in docs.
For area I can’t find anything, and in source get_content_area
noted three times without function definition. Moreover, I can’t find it in a gtk-rs repo as well! That’s intriguing.
let popup = gtk::Dialog::new_with_buttons(
...
let area = popup.get_content_area();
So it looks like a method for dialog
… This is GtkDialog, and it has gtk_dialog_get_content_area ()
. It returns the content area GtkBox. Now we know that ‘area’ is a Box
— A container for packing widgets in a single row or column. [wow! Total twist! I though that ‘area’ is where they draw. No, box is just a container for widgets, so it’s a higher level stuff than I need. Good to know.]
scroll — GtkScrolledWindow
GtkScrolledWindow — Adds scrollbars to its child widget
let scroll = gtk::ScrolledWindow::new(None::<>k::Adjustment>, None::<>k::Adjustment>);
...
scroll.add(&vertical_layout); scroll.connect_show(clone!(ram_usage_history, cpu_usage_history => move |_| {
ram_usage_history.borrow().show_all();
cpu_usage_history.borrow().show_all();
}));
notebook.create_tab("Resources usage", &scroll);
Again, miss. We don’t need this.
I feel like I missed something.
let area = graph.area.clone();
area.connect_draw(clone!(graph => move |w, c| {
graph.borrow()
.draw(...}));
})
graph.area.clone
— what is this area?
use gtk::DrawingArea
pub struct Graph {
...
pub area: DrawingArea,
...
}
...
area: DrawingArea::new(),
GtkDrawingArea sounds like the place I want to draw onto.
So I need create a window, attach a GTKDrawingArea
to it, draw on it and update it as needed.
Can I just attach it directly to the window?
Coding time
… I’ve stuck almost instantly. How I add DrawArea to the Window? I found this example.
There I can see less intimidating chain of actions (but gosh, it’s hard to jump between C and Rust).
window = gtk_application_window_new (app);
frame = gtk_frame_new (NULL);
gtk_container_add (GTK_CONTAINER (window), frame);
drawing_area = gtk_drawing_area_new ();
gtk_container_add (GTK_CONTAINER (frame), drawing_area);
Now I need to translate this into gtk-rs/Rust.
let win = gtk::ApplicationWindow::new(app);
let frame = gtk::Frame::new(None);
let area = DrawingArea::new();
Good, good, almost there. Where is gtk_container_add? Should it be an associated function for window or gtk::container_add
?
Neither.
A little peek into process explorer…
frame.add(&frame)
compiles! One small step for the man, and humanity does not care.
frame.add(&area);
win.add(&frame);
Two lines done. All I need now is some proof of work. Draw something. As far as I understand, I need to attach some callback to handle draw events…
area.connect_draw(move|w, c|{
println!("draw");
gtk::Inhibit(false)
});
It prints! I’m getting closer and closer. c
is Context
(Cairo context?), and w
is self
, as far as I understood. In my case it’s a DrawingArea
which my closure is attached to.
Draw it!
c.rectangle(1.0, 1.0, 100.0, 200.0);
c.fill();
And, finally, we’ve got our first rectangle on the screen. Good! Here is my code.
Next thing is to react to resize. To see what’s going on, I need to somehow show that I’ve updated the screen. My idea is to use random colors on each draw.
Random, you said…
The next error have thrown me away from GTK and directly into LEARNING RUST.
The code:
let mut rng = rand::thread_rng();
area.connect_draw(move|w, c|{
println!("w: {} c:{}",w, c);
c.set_source_rgb(
rng.gen_range(0.0, 1.0),
rng.gen_range(0.0, 1.0),
rng.gen_range(0.0, 1.0)
);
c.rectangle(1.0, 1.0, 100.0, 200.0);
c.fill();
gtk::Inhibit(false)
});
The error:
error[E0596]: cannot borrow `rng` as mutable, as it is a captured variable in a `Fn` closure
--> src/main.rs:25:30
|
25 | c.set_source_rgb(rng.gen_range(0.0, 1.0), rng.gen_range(0.0, 1.0), rng.gen_range(0.0, 1.0));
| ^^^ cannot borrow as mutable
|
help: consider changing this to accept closures that implement `FnMut`
--> src/main.rs:23:27
|
23 | area.connect_draw(move|w, c|{
| ___________________________^
24 | | println!("w: {} c:{}",w, c);
25 | | c.set_source_rgb(rng.gen_range(0.0, 1.0), rng.gen_range(0.0, 1.0), rng.gen_range(0.0, 1.0));
26 | | c.rectangle(1.0, 1.0, 100.0, 200.0);
27 | | c.fill();
28 | | gtk::Inhibit(false)
29 | | });
| |_________^
Huh. Closures, Fn-traits and borrow-checker at theirs best.
Funny enough, the simpler version does work:
let clo = move ||{rng.gen_range(0.0, 1.0)};
My guess is that connect_draw
wants to have closure with Fn
trait, not with Fn-mut
.
Yes, I’ve made a simpler example and found it have an exactly the same problem. Why connect_draw
wants Fn, not Fn-mut? Is there any specific reason, or it’s just an oversight? I think, it’s time to read the docs. Oh. They said ‘RefCell
’, and whilst I watched a video or two on them, and read about them, I don’t like them. There is something fishy …unsafe hidden inside the interior mutability idea. Nevertheless, let’s use it.
Ah… It worked. Happiness!
let mut rng = RefCell::new(rand::thread_rng());
area.connect_draw(move|w, c|{
println!("w: {} c:{}",w, c);
let r = rng.borrow_mut().gen_range(0.0, 1.0);
let g = rng.borrow_mut().gen_range(0.0, 1.0);
let b = rng.borrow_mut().gen_range(0.0, 1.0);
c.set_source_rgb(r, g, b);
c.rectangle(1.0, 1.0, 100.0, 200.0);
c.fill();
gtk::Inhibit(false)
});
I still have troubles to understand gtk-rs explanation for reasons why it should be RefCell
.
Anyway, now I can see a shiny rainbow on resize. And it’s not good, because I redraw the whole screen on each time. We need to learn how to redraw the invalidated part only.
Should I take a break?
It was kinda hard. I’m right now start to dig into invalidation topic, but a bulk of text above pushing me down. Even this thing sits in my drafts happily, it still pressure me. So, I’m posting this part (I’ve added ‘part 1’ to the title just now) as it is, with no final solution in it. Stay tuned for the next part.
My code at this moment is at this commit. My next goals are to support partial redraw (invalidation), have a separate code to draw something continuously (while handling partial redraws due to external reasons), and add some GUI elements to the window. Some dropbox and a button would be enough for now.