I am currently learning Rust and looking to use it for developing a GUI based application with GTK+. My problem relates to registering callbacks to respond to GTK events/signals and mutating state within those callbacks. I have a working but inelegant solution, so I would like to ask if there is a cleaner, more idiomatic solution.
I have implemented my code as a struct with method implementations, where
the struct maintains references to the GTK widgets along with other state
that it needs. It constructs a closure that is passed to the
GtkWidget::connect* functions in order to receive events, draw to a
canvas, etc. This can cause problems with the borrow checker, as I will now
explain. I have some working but (IMHO) non-ideal code that I will
show.
Initial, non-working solution:
#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]
extern crate gtk;
extern crate cairo;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};
struct RenderingAPITestWindow {
window: gtk::Window,
drawing_area: gtk::DrawingArea,
width: i32,
height: i32
}
impl RenderingAPITestWindow {
fn new(width: i32, height: i32) -> RenderingAPITestWindow {
let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
let drawing_area = gtk::DrawingArea::new().unwrap();
drawing_area.set_size_request(width, height);
window.set_title("Cairo API test");
window.add(&drawing_area);
let instance = RenderingAPITestWindow{window: window,
drawing_area: drawing_area,
width: width,
height: height,
};
instance.drawing_area.connect_draw(|widget, cairo_context| {
instance.on_draw(cairo_context);
instance.drawing_area.queue_draw();
Inhibit(true)
});
instance.drawing_area.connect_size_allocate(|widget, rect| {
instance.on_size_allocate(rect);
});
instance.window.show_all();
return instance;
}
fn exit_on_close(&self) {
self.window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(true)
});
}
fn on_draw(&mut self, cairo_ctx: Context) {
cairo_ctx.save();
cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
cairo_ctx.set_font_size(18.0);
cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
cairo_ctx.restore();
}
fn on_size_allocate(&mut self, rect: &RectangleInt) {
self.width = rect.width as i32;
self.height = rect.height as i32;
}
}
fn main() {
gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());
let window = RenderingAPITestWindow::new(800, 500);
window.exit_on_close();
gtk::main();
}
The above fails to compile as the closures with
RenderingAPITestWindow::new that are created and passed to calls to
GtkWidget::connect* methods attempt to borrow instance. The
compiler states that the closures may outlive the function in which
they are declared and that instance is owned by the outer function,
hence the problem. Given that GTK may keep a reference to these closures
around for an unspecified amount of time, we need an approach in which the
lifetime can be determined at runtime, hence my next stab at the problem
in which the RenderingAPITestWindow instance is wrapped in
Rc<RefCell<...>>.
Wrapping the RenderingAPITestWindow instance compiles but dies at runtime:
#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]
extern crate gtk;
extern crate cairo;
use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};
struct RenderingAPITestWindow {
window: gtk::Window,
drawing_area: gtk::DrawingArea,
width: i32,
height: i32
}
impl RenderingAPITestWindow {
fn new(width: i32, height: i32) -> Rc<RefCell<RenderingAPITestWindow>> {
let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
let drawing_area = gtk::DrawingArea::new().unwrap();
drawing_area.set_size_request(width, height);
window.set_title("Cairo API test");
window.add(&drawing_area);
let instance = RenderingAPITestWindow{window: window,
drawing_area: drawing_area,
width: width,
height: height,
};
let wrapped_instance = Rc::new(RefCell::new(instance));
let wrapped_instance_for_draw = wrapped_instance.clone();
wrapped_instance.borrow().drawing_area.connect_draw(move |widget, cairo_context| {
wrapped_instance_for_draw.borrow_mut().on_draw(cairo_context);
wrapped_instance_for_draw.borrow().drawing_area.queue_draw();
Inhibit(true)
});
let wrapped_instance_for_sizealloc = wrapped_instance.clone();
wrapped_instance.borrow().drawing_area.connect_size_allocate(move |widget, rect| {
wrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);
});
wrapped_instance.borrow().window.show_all();
return wrapped_instance;
}
fn exit_on_close(&self) {
self.window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(true)
});
}
fn on_draw(&mut self, cairo_ctx: Context) {
cairo_ctx.save();
cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
cairo_ctx.set_font_size(18.0);
cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
cairo_ctx.restore();
}
fn on_size_allocate(&mut self, rect: &RectangleInt) {
self.width = rect.width as i32;
self.height = rect.height as i32;
}
}
fn main() {
gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());
let wrapped_window = RenderingAPITestWindow::new(800, 500);
wrapped_window.borrow().exit_on_close();
gtk::main();
}
The above solution compiles but its not particularly pretty:
RenderingAPITestWindow::newreturns anRc<RefCell<RenderingAPITestWindow>>rather than aRenderingAPITestWindow- Accessing fields and methods of
RenderingAPITestWindowis complicated by the fact that theRc<RefCell<...>>must be opened up; it now requireswrapped_instance.borrow().some_method(...)rather than justinstance.some_method(...) - Each closure requires it's own clone of
wrapped_instance; attempting to usewrapped_instancewould attempt to borrow an object -- the wrapper rather than theRenderingAPITestWindowthis time -- that is owned byRenderingAPITestWindow::newas before
While the above compiles, it dies at runtime with:
thread '<main>' panicked at 'RefCell<T> already borrowed', ../src/libcore/cell.rs:442
An unknown error occurred
This is due to the fact that the call to window.show_all() causes GTK to
initialise the widget hierarchy, resulting in the drawing area widget
receiving a size-allocate event. Accessing the window to call
show_all() required that the Rc<RefCell<...>> is opened (hence
wrapped_instance.borrow().window.show_all();) and the instance
borrowed. Before the borrow ends when show_all() returns, GTK invokes the
drawing area's size-allocate event handler, which causes the closure
connected to it (4 lines above) to be invoked. The closure attempts to
borrow a mutable reference to the RenderingAPITestWindow instance
(wrapped_instance_for_sizealloc.borrow_mut().on_size_allocate(rect);)
in order to invoke the on_size_allocate method. This attempts to borrow a
mutable reference, while the first immutable reference is still in scope.
This second borrow causes the run-time panic.
The working but - IMHO - inelegant solution that I have managed to get
working so far is to split RenderingAPITestWindow into two structs, with
the mutable state that is to modified by the callbacks moved into a
separate struct.
Working but inelegant solution that splits the RenderingAPITestWindow struct:
#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]
extern crate gtk;
extern crate cairo;
use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};
struct RenderingAPITestWindowState {
width: i32,
height: i32
}
impl RenderingAPITestWindowState {
fn new(width: i32, height: i32) -> RenderingAPITestWindowState {
return RenderingAPITestWindowState{width: width, height: height};
}
fn on_draw(&mut self, cairo_ctx: Context) {
cairo_ctx.save();
cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
cairo_ctx.set_font_size(18.0);
cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
cairo_ctx.restore();
}
fn on_size_allocate(&mut self, rect: &RectangleInt) {
self.width = rect.width as i32;
self.height = rect.height as i32;
}
}
struct RenderingAPITestWindow {
window: gtk::Window,
drawing_area: gtk::DrawingArea,
state: Rc<RefCell<RenderingAPITestWindowState>>
}
impl RenderingAPITestWindow {
fn new(width: i32, height: i32) -> Rc<RefCell<RenderingAPITestWindow>> {
let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
let drawing_area = gtk::DrawingArea::new().unwrap();
drawing_area.set_size_request(width, height);
window.set_title("Cairo API test");
window.add(&drawing_area);
let wrapped_state = Rc::new(RefCell::new(RenderingAPITestWindowState::new(width, height)))
;
let instance = RenderingAPITestWindow{window: window,
drawing_area: drawing_area,
state: wrapped_state.clone()
};
let wrapped_instance = Rc::new(RefCell::new(instance));
let wrapped_state_for_draw = wrapped_state.clone();
let wrapped_instance_for_draw = wrapped_instance.clone();
wrapped_instance.borrow().drawing_area.connect_draw(move |widget, cairo_context| {
wrapped_state_for_draw.borrow_mut().on_draw(cairo_context);
wrapped_instance_for_draw.borrow().drawing_area.queue_draw();
Inhibit(true)
});
let wrapped_state_for_sizealloc = wrapped_state.clone();
wrapped_instance.borrow().drawing_area.connect_size_allocate(move |widget, rect| {
wrapped_state_for_sizealloc.borrow_mut().on_size_allocate(rect);
});
wrapped_instance.borrow().window.show_all();
return wrapped_instance;
}
fn exit_on_close(&self) {
self.window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(true)
});
}
}
fn main() {
gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());
let wrapped_window = RenderingAPITestWindow::new(800, 500);
wrapped_window.borrow().exit_on_close();
gtk::main();
}
While the above code works as required, I would like to find a better way
for going forward; I would like to ask if anyone knows a better approach as
the above complicates the programming process a fair bit, with the need to
use Rc<RefCell<...>> and split structs to satisfy Rust's borrowing rules.
Here's a working version that I came up with:
#![cfg_attr(not(feature = "gtk_3_10"), allow(unused_variables, unused_mut))]
extern crate gtk;
extern crate cairo;
use std::rc::Rc;
use std::cell::RefCell;
use gtk::traits::*;
use gtk::signal::Inhibit;
use cairo::{Context, RectangleInt};
struct RenderingAPITestWindow {
window: gtk::Window,
drawing_area: gtk::DrawingArea,
state: RefCell<RenderingState>,
}
struct RenderingState {
width: i32,
height: i32,
}
impl RenderingAPITestWindow {
fn new(width: i32, height: i32) -> Rc<RenderingAPITestWindow> {
let window = gtk::Window::new(gtk::WindowType::TopLevel).unwrap();
let drawing_area = gtk::DrawingArea::new().unwrap();
drawing_area.set_size_request(width, height);
window.set_title("Cairo API test");
window.add(&drawing_area);
let instance = Rc::new(RenderingAPITestWindow {
window: window,
drawing_area: drawing_area,
state: RefCell::new(RenderingState {
width: width,
height: height,
}),
});
{
let instance2 = instance.clone();
instance.drawing_area.connect_draw(move |widget, cairo_context| {
instance2.state.borrow().on_draw(cairo_context);
instance2.drawing_area.queue_draw();
Inhibit(true)
});
}
{
let instance2 = instance.clone();
instance.drawing_area.connect_size_allocate(move |widget, rect| {
instance2.state.borrow_mut().on_size_allocate(rect);
});
}
instance.window.show_all();
instance
}
fn exit_on_close(&self) {
self.window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(true)
});
}
}
impl RenderingState {
fn on_draw(&self, cairo_ctx: Context) {
cairo_ctx.save();
cairo_ctx.move_to(50.0, (self.height as f64) * 0.5);
cairo_ctx.set_font_size(18.0);
cairo_ctx.show_text("The only curse they could afford to put on a tomb these days was 'Bugger Off'. --PTerry");
cairo_ctx.restore();
}
fn on_size_allocate(&mut self, rect: &RectangleInt) {
self.width = rect.width as i32;
self.height = rect.height as i32;
}
}
fn main() {
gtk::init().unwrap_or_else(|_| panic!("Failed to initialize GTK."));
println!("Major: {}, Minor: {}", gtk::get_major_version(), gtk::get_minor_version());
let window = RenderingAPITestWindow::new(800, 500);
window.exit_on_close();
gtk::main();
}
I arrived at this through a few observations:
- The instance is being shared across multiple closures for an undetermined amount of time.
Rcis the right answer to that scenario because it provides shared ownership.Rcis very ergonomic to use; it works like any other pointer type. - The only part of
instancethat is actually mutated is your state. Since your instance is being shared, it cannot be borrowed mutably using the standard&mutpointer. Therefore, you must use interior mutability. This is whatRefCellprovides. Note though, that you only need to useRefCellon the state you're mutating. So this still separates out the state into a separate struct, but it works nicely IMO. - A possible modification to this code is to add
#[derive(Clone, Copy)]to the definition of theRenderingStatestruct. Since it can beCopy(because all of its component types areCopy), you can useCellinstead ofRefCell.
来源:https://stackoverflow.com/questions/31966497/howto-idiomatic-rust-for-callbacks-with-gtk-rust-gnome