Rust draw and update in window

Viewed 598

I'm very new to Rust and rarely do any kind of graphics or UI stuff, so expect dumb mistakes.

I am trying to create a program that runs a simulation and displays its changing state over time. For this I want to create a window and then draw stuff on it, and have the stuff that's drawn change. Ideally it would be great if it were optimized for placing individual pixels (with the expectation that most of the time only a few pixels will change per timestep), but for now I'll settle for anything where I can run some code, display the result, run some more code, and refresh the display.

What I have so far:

fn main() {
let app = Application::builder()
    .application_id("org.example.HelloWorld")
    .build();

app.connect_activate(|app| {
    let win = ApplicationWindow::builder()
        .application(app)
        .default_width(320)
        .default_height(200)
        .title("Hello, World!")
        .build();

    let draw_area = DrawingArea::new();
    win.add(&draw_area);
    draw_area.connect_draw(|_unused, f| {
        f.set_source_rgb(1.0, 0.0, 0.0);
        f.paint();
        thread::sleep(time::Duration::from_millis(1000));
        f.set_source_rgb(0.0, 1.0, 0.0);
        f.paint();
        Inhibit(false)
    });
    win.show_all();
});
app.run();
}

I would like this to show an all-red window, wait for a second, and then show an all-green one. Instead it waits for a second and shows an all-green window. How can I restructure it so that I can update the display at intervals I control?

2 Answers

I don't know GTK, but most GUI systems have some fairly common properties here so I'll give you a generic answer since no one else has yet:

You need to return control to the event loop any time you want “time to pass” properly. That is, never use thread::sleep; if you want to do something later, use the event loop's timer mechanism that will call a callback later. (I couldn't find where the gtk crate has one of those, but I'm almost certain it will.)

Also, you need to consider that your draw callback will, probably, be called whenever the window is resized for the purpose of completely redrawing. It is rare for a GUI to offer a “retained” widget that remembers what pixels it should display across resize events; usually, you need your own image buffer to do that if you want it, and for most animation purposes, the conventional thing to do is to redraw everything whenever you want it to change. If you have an application that is slowly placing specific pixels, you should probably keep your own image buffer.

You can ask a drawing area to (re)draw itself by calling queue_draw (Source : here)

Draw signals are normally delivered when a drawing area first comes onscreen, or when it’s covered by another window and then uncovered. You can also force an expose event by adding to the “damage region” of the drawing area’s window; WidgetExt::queue_draw_area() and Window::invalidate_rect() are equally good ways to do this. You’ll then get a draw signal for the invalid region.

Knowing this, you could bind a call to queue_draw to a timer, which would tick every p, where p would be a time period of your choice. I did it in the past and it works fine. In C, I used Glib::SignalTimeout::connect (I don't know what the equivalent is in gtk-rs).

So, in conclusion, every p milliseconds, queue_draw would be called on your drawing area which would trigger its draw function (the callable parameter you passed to connect_draw) and redraw it. If you do this, remove this line:

thread::sleep(time::Duration::from_millis(1000));

which only blocks the main loop.

Related