Generic Select

<
>
January 2, 2022

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.