Hello,
i am writing a UI program and have a couple of so called Pages which hold a lot of UI Widgets. I want to iterate over those Widgtes which implement a common trait (e.g. for drawing) and still be able to access them individually with their real type (e.g. for setting their value).
The UI is keyboard centric, so the user can switch to a different Widget with keypresses. To enable that I give each Widget in a Page a unique number which the other widgets then can refer to. This design works really well for the system. Now i want to make sure that the indices i give to other widgets and the index each widget has in the list don't go out of sync.
struct Page {
current_widget: usize,
text_input: TextIn,
button: Button,
}
impl Page {
const WIDGET_COUNT: usize = 2;
const TEXT: usize = 0;
const BUTTON: usize = 1;
// indices don't correspond to array index
// especially with more widgets (10+) it becomes hard to spot mistakes
fn array_1(&mut self) -> [&mut dyn Widget; Self::WIDGET_COUNT] {
[&mut self.text_input, &mut self.button]
}
// really happy with this design. doesn't compile because of lifetime issues with FnMut.
// "captured variable cannot escape `FnMut` closure body"
// if you know how to work around this i would be very happy.
fn array_2(&mut self) -> [&mut dyn Widget; Self::WIDGET_COUNT] {
std::array::from_fn(|idx| match idx {
Self::TEXT => &mut self.text_input as &mut dyn Widget,
Self::BUTTON => &mut self.button as &mut dyn Widget,
_ => panic!()
})
}
// not nice because of unsafe. could forget to init one
// easy to spot things going out of sync
fn array_3(&mut self) -> [&mut dyn Widget; Self::WIDGET_COUNT] {
let mut array = [const { MaybeUninit::<&mut dyn Widget>::uninit() }; Self::WIDGET_COUNT];
array[Self::QUIT_BUTTON].write(&mut self.quit_button);
array[Self::TEXT_IN].write(&mut self.text_in);
unsafe {
std::mem::transmute(array)
}
}
}
I have written a test which could catch swapped elements in design 1 and not initialzed elements in design 3, but i would still prefer to have the relationship between the constants and the list directly in the code.
I could probably write a macro, but i think a proc_macro would be needed and i have never written one before so i would prefer to avoid that.
That wouldn't really help sadly. Here the order is inverted and it is not visible from the list definition. As soon as there are multiple Widgets of the same type it gets even harder.
A simple alternative to indexing an array is a fn that matches on the cost input param and returns a single widget ref. It would be roughly just as fast. Assuming indexing is all you need.
error: captured variable cannot escape `FnMut` closure body
--> src/visual/ui/pages/help_page.rs:27:43
|
22 | fn draw(&mut self, draw_buffer: &mut DrawBuffer) {
| --------- variable defined here
...
27 | (0..Self::WIDGET_COUNT).map(|idx| self.get_widget(idx)).for_each(|widget| widget.draw(draw_buffer, true));
| - ----^^^^^^^^^^^^^^^^
| | |
| | returns a reference to a captured variable which escapes the closure body
| | variable captured here
| inferred to be a `FnMut` closure
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
If you've heard of "lending iterators" and why they cannot work with the Iterator trait, closures[1] have the same limitation. This means it's not (safely or soundly) possible to capture &mut self in a FnMut and hand out &mut self.button for example.[2]
ponders
This creates all the &mut ahead of time, moves them into the closure, and (fallibly from a type checking perspective) returns them based on the index.
No i haven't heard of lending Iterators but i noticed that i couldn't do the same lifetime pattern as the slice Iter for example has. I think that is basically that issue, right?
That code looks great, thank you! It's a way to do my design 3 safely, nice!
Oh great. Yes this works, thank you! Didn't think about solving the FnMut issue that way. I think i prefer this over the array honestly as i don't have to "collect" the array first.
Yeah, it's the same underlying reason as why a mutable slice iterator requires a special dance.
When you call next you have a &mut self to the iterator, but the returned item must be independent of the lifetime on the &mut self.
When you call a FnMut you have a &mut self to the closure, but the returned value must be independent of the lifetime on the &mut self.
The reason is they both use a non-generic associated type across all &mut calls.
trait Iterator {
type Item;
// same `Item` type every call (can't rely on `&mut self` lifetime)
fn next(&mut self) -> Self::Item;
}
// (slightly simplified for exposition)
trait FnOnce<Args> {
type Output;
fn call_once(self, args: Args) -> Self::Output;
}
trait FnMut<Args>: FnOnce<Args> {
// same `Output` type every call (can't rely on `&mut self` lifetime)
fn call_mut(&mut self, args: Args) -> Self::Output;
}
Shared references avoid the problem because you can copy the inner reference out without involving ("reborrowing") &mut self, so all the returned shared references can have the same lifetime independent of the lifetime on &mut self.