Generic Select
I kept adding more and more ‘Select’ UIs such as the Item Select
, Structure Select
and the Research
view. Thanks to this I duplicated a lot of code.
I wanted to generalize this for quite some time to make it easier to add new UIs or update existing ones.
I introduced a trait which has to be implemented by the specialized UIs:
pub trait ElementAccessor<E> {
fn n_elements(&self) -> usize;
fn get(&self, i: usize) -> Option<&E>;
fn visual_size(&self, e: &E) -> Size<u32>;
fn text_info(&self, e: &E) -> Option<TextRenderInfo>;
fn node_for(&self, e: &E, pos_x: f32, pos_y: f32) -> Option<Node>;
}
Where TextRenderInfo
contains information required for later text rendering:
pub struct TextRenderInfo {
pub text: String,
pub center_horizontally: bool,
pub center_vertically: bool,
}
GenericSelect
is then able to render a proper UI for any ElementAccessor
:
pub struct GenericSelect<'a, E, EA>
where
EA: ElementAccessor<E>,
{
pub element_accessor: EA,
pub depth: Depth,
pub core: CoreUI,
pub cursor: &'a Cursor,
pub phantom: PhantomData<E>,
}
impl<'a, E, EA> GenericSelect<'a, E, EA>
where
EA: ElementAccessor<E>,
{
pub fn render_node(&self) -> Node {
...
}
...
}
While this is useful for all the ‘listing’ UIs, the Planet
and Blueprint
still shared code with GenericSelect
to e.g. convert between screen and game sizes/coordinates, or to track the camera position, size and zoom.
For this I added:
pub struct CoreUI {
pub scale_factor: f64,
pub size: Size<Screen<u32>>,
pub camera_pos: Pos<f64>,
pub zoom: f64,
}
impl CoreUI {
pub fn to_screen<T>(&self, x: T) -> Screen<f64>
where
T: Into<f64>,
{
...
}
pub fn unscreen<T>(&self, x: Screen<T>) -> f64
...
}
Which holds the state mentioned above and offers conversion functions.
With all those helpers, something like the Item Select
can now be implemented in very few lines of code:
pub struct ItemAccessor<'a> {
pub items: &'a [Item],
}
impl<'a> ElementAccessor<Item> for ItemAccessor<'a> {
fn n_elements(&self) -> usize {
self.items.len()
}
fn get(&self, i: usize) -> Option<&Item> {
self.items.get(i)
}
fn visual_size(&self, _e: &Item) -> Size<u32> {
[1, 1].into()
}
fn text_info(&self, _e: &Item) -> Option<TextRenderInfo> {
None
}
fn node_for(&self, e: &Item, pos_x: f32, pos_y: f32) -> Option<Node> {
item_node(e, translation(pos_x, pos_y, 0.0), Depth::Above(1))
}
}
...
pub struct ItemSelect<'a> {
pub generic_select: GenericSelect<'a, Item, ItemAccessor<'a>>,
pub notifier: &'a dyn Notifier,
}
impl<'a> ItemSelect<'a> {
pub fn render_node(&self) -> Node {
self.generic_select.render_node()
}
}
impl<'a> Converter<UIEvent, Option<ClientEvent>> for ItemSelect<'a> {
fn convert(&self, event: UIEvent) -> Option<ClientEvent> {
match event {
UIEvent::Click(pos) => {
if let Some(item) = self.generic_select.element_at(pos) {
self.notifier
.notify_str(format!("set cursor item to {:?}", item));
Some(ClientEvent::SetCursorItem(Some(*item)))
} else {
self.notifier.notify_str(format!("reset cursor item"));
Some(ClientEvent::SetCursorItem(None))
}
}
_ => None,
}
}
}
Strong types for sizes and positions
As you might have noticed above I added more types to for example tag whether a value or position is in screen or world (not tagged) coordinates. This makes it impossible to accidentally mix up units by for example adding pixel coordinates to world coordinates.