I'm currently working on a small status bar for Sway WM. The bar is composed of multiple objects implementing a Widget
trait.
Amongst all widgets, the clock is a little special. I don't just want to render it, but I also want to ensure its rendering is done in a timely fashion, otherwise the time displayed often ends up being a little off. Due to this requirement, I need to both work with the clock as a Widget
and also as its own Clock
.
The issue I'm facing is that once the clock widget is stored in a vector with the other widgets, the borrow checker does not allow any mutable references outside of the vector to exist. This made it impossible to use the clock as an instance of Clock
within the main application loop.
The cleanest way I found to solve the issue, without introducing runtime overhead, was through raw pointers and unsafe code. I'd like to know of other zero runtime overhead solutions to this problem.
Here's an over-simplified version of the code that illustrates what the current code looks like:
trait Widget {
fn update_info(&mut self);
fn render(&self);
}
struct NetworkStatus{}
impl Widget for NetworkStatus {
fn update_info(&mut self) {
// query the OS for network information
}
fn render(&self) {
println!("Connected wirelessly");
}
}
struct Clock{}
impl Widget for Clock {
fn update_info(&mut self) {
// query the OS for the current time
}
fn render(&self) {
println!("2024-03-06T19:25:17");
}
}
impl Clock {
fn get_alignment_delay(&mut self) -> std::time::Duration {
return std::time::Duration::new(1, 0);
}
}
fn main() {
let mut netstat = NetworkStatus{};
let mut clock = Clock{};
let clock_ptr = &mut clock as *mut Clock;
let mut widgets: Vec<&mut dyn Widget> = vec![
&mut netstat,
&mut clock,
];
loop {
for w in widgets.iter_mut() {
w.update_info();
}
for w in widgets.iter() {
w.render();
}
std::thread::sleep(unsafe {
(*clock_ptr).get_alignment_delay()
});
}
}
I'm aware that different architectures could solve the issue, but I'm just interested in learning some solutions for this exact problem so that I'll know what to do the next time I see something similar.
Rust doesn't allow uncontrolled, shared, mutable access. For your application, you need shared, mutable access to the clock, so you have to pick a way to control it. There are many possibilities with different consequences.
You could put each widget in a RefCell
, Mutex
or RwLock
to allow runtime-checked mutation.
fn main() {
let netstat = RefCell::new(NetworkStatus{});
let clock = RefCell::new(Clock{});
// note these are now `&` references, which may be shared
let widgets: Vec<&RefCell<dyn Widget>> = vec![
&netstat,
&clock,
];
loop {
for w in widgets.iter() {
w.borrow_mut().update_info();
}
for w in widgets.iter() {
w.borrow().render();
}
std::thread::sleep(clock.borrow().get_alignment_delay());
}
}
You could make a note of where in the vector the clock is, then access it as widgets[index_of_the_clock].get_alignment_delay()
. This might make sense if users can add and remove multiple animated widgets and you want to check with all of them how long to sleep.
You could require every widget to be interior-mutable (that is, to use things like RefCell
for all mutable parts of itself) so that update()
only needs &self
instead of &mut self
. Then you can share access to the widgets with &
like in the code sample above, but without needing the wrapping RefCell
. (This is probably less ergonomic than the first option.)
Note that in all of these cases, you may find that using a vector of references is limiting -- in particular, if you want to let the user add new widgets while the program is running, the vector should really own the widgets rather than borrowing them. This means the type of the vector would be Vec<Box<dyn Widget>>
(or in case 1, Vec<Box<RefCell<dyn Widget>>>
). This doesn't stop you from mutating the widgets because ownership includes the ability to mutate.