#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ContentId {
StopSelector,
StopLabels,
LightnessSlider,
ChromaSlider,
HueSlider,
HintArrows,
HintAdjust,
HintTab,
GradientStrip,
ColorInfo,
TabIndicators,
TabLabels,
SensorAngleRow,
SensorDistanceRow,
TurnAngleRow,
StepSizeRow,
DecayRow,
DepositRow,
TimeScaleRow,
FooterHints,
FpsRow,
TrailRow,
EntropyRow,
CpuRow,
MemoryRow,
Title,
Separator,
Empty,
Custom(&'static str),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RowType {
Empty,
Content(ContentId),
Separator,
Title,
}
#[derive(Debug, Clone)]
pub struct OverlayLayout {
rows: Vec<RowType>,
}
impl Default for OverlayLayout {
fn default() -> Self {
Self::new()
}
}
impl OverlayLayout {
pub fn new() -> Self {
Self { rows: Vec::new() }
}
pub fn add_empty(&mut self) -> usize {
let idx = self.rows.len();
self.rows.push(RowType::Empty);
idx
}
pub fn add_title(&mut self) -> usize {
let idx = self.rows.len();
self.rows.push(RowType::Title);
idx
}
pub fn add_separator(&mut self) -> usize {
let idx = self.rows.len();
self.rows.push(RowType::Separator);
idx
}
pub fn add_content(&mut self, id: ContentId) -> usize {
let idx = self.rows.len();
self.rows.push(RowType::Content(id));
idx
}
pub fn row_of(&self, id: ContentId) -> Option<usize> {
self.rows
.iter()
.position(|row| matches!(row, RowType::Content(row_id) if *row_id == id))
}
pub fn rows_of(&self, id: ContentId) -> Vec<usize> {
self.rows
.iter()
.enumerate()
.filter_map(|(idx, row)| {
if matches!(row, RowType::Content(row_id) if *row_id == id) {
Some(idx)
} else {
None
}
})
.collect()
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn row_type(&self, index: usize) -> Option<RowType> {
self.rows.get(index).copied()
}
pub fn contains(&self, id: ContentId) -> bool {
self.row_of(id).is_some()
}
pub fn iter(&self) -> impl Iterator<Item = (usize, RowType)> + '_ {
self.rows.iter().enumerate().map(|(idx, row)| (idx, *row))
}
pub fn first_of_category<F>(&self, predicate: F) -> Option<usize>
where
F: Fn(ContentId) -> bool,
{
self.rows.iter().enumerate().find_map(|(idx, row)| {
if let RowType::Content(id) = row {
if predicate(*id) {
return Some(idx);
}
}
None
})
}
}
pub trait ContentIdExt {
fn is_slider(&self) -> bool;
fn is_hint(&self) -> bool;
fn is_parameter(&self) -> bool;
}
impl ContentIdExt for ContentId {
fn is_slider(&self) -> bool {
matches!(
self,
ContentId::LightnessSlider | ContentId::ChromaSlider | ContentId::HueSlider
)
}
fn is_hint(&self) -> bool {
matches!(
self,
ContentId::HintArrows | ContentId::HintAdjust | ContentId::HintTab
)
}
fn is_parameter(&self) -> bool {
matches!(
self,
ContentId::SensorAngleRow
| ContentId::SensorDistanceRow
| ContentId::TurnAngleRow
| ContentId::StepSizeRow
| ContentId::DecayRow
| ContentId::DepositRow
| ContentId::TimeScaleRow
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_layout() {
let layout = OverlayLayout::new();
assert_eq!(layout.row_count(), 0);
assert_eq!(layout.row_of(ContentId::LightnessSlider), None);
}
#[test]
fn test_add_rows() {
let mut layout = OverlayLayout::new();
let empty_idx = layout.add_empty();
let title_idx = layout.add_title();
let sep_idx = layout.add_separator();
let content_idx = layout.add_content(ContentId::LightnessSlider);
assert_eq!(empty_idx, 0);
assert_eq!(title_idx, 1);
assert_eq!(sep_idx, 2);
assert_eq!(content_idx, 3);
assert_eq!(layout.row_count(), 4);
}
#[test]
fn test_row_lookup() {
let mut layout = OverlayLayout::new();
layout.add_empty();
layout.add_content(ContentId::LightnessSlider);
layout.add_content(ContentId::ChromaSlider);
layout.add_empty();
layout.add_content(ContentId::HueSlider);
assert_eq!(layout.row_of(ContentId::LightnessSlider), Some(1));
assert_eq!(layout.row_of(ContentId::ChromaSlider), Some(2));
assert_eq!(layout.row_of(ContentId::HueSlider), Some(4));
assert_eq!(layout.row_of(ContentId::StopSelector), None);
}
#[test]
fn test_contains() {
let mut layout = OverlayLayout::new();
layout.add_content(ContentId::LightnessSlider);
assert!(layout.contains(ContentId::LightnessSlider));
assert!(!layout.contains(ContentId::ChromaSlider));
}
#[test]
fn test_rows_of_multiple() {
let mut layout = OverlayLayout::new();
layout.add_content(ContentId::HintArrows);
layout.add_content(ContentId::HintAdjust);
layout.add_content(ContentId::HintTab);
let hint_rows: Vec<_> = layout
.iter()
.filter(|(_, row)| matches!(row, RowType::Content(id) if id.is_hint()))
.map(|(idx, _)| idx)
.collect();
assert_eq!(hint_rows, vec![0, 1, 2]);
}
#[test]
fn test_content_id_ext() {
assert!(ContentId::LightnessSlider.is_slider());
assert!(ContentId::ChromaSlider.is_slider());
assert!(ContentId::HueSlider.is_slider());
assert!(!ContentId::StopSelector.is_slider());
assert!(ContentId::HintArrows.is_hint());
assert!(!ContentId::LightnessSlider.is_hint());
assert!(ContentId::SensorAngleRow.is_parameter());
assert!(!ContentId::LightnessSlider.is_parameter());
}
#[test]
fn test_row_type_access() {
let mut layout = OverlayLayout::new();
layout.add_empty();
layout.add_title();
layout.add_separator();
layout.add_content(ContentId::LightnessSlider);
assert_eq!(layout.row_type(0), Some(RowType::Empty));
assert_eq!(layout.row_type(1), Some(RowType::Title));
assert_eq!(layout.row_type(2), Some(RowType::Separator));
assert_eq!(
layout.row_type(3),
Some(RowType::Content(ContentId::LightnessSlider))
);
assert_eq!(layout.row_type(99), None);
}
#[test]
fn test_first_of_category() {
let mut layout = OverlayLayout::new();
layout.add_empty();
layout.add_content(ContentId::SensorAngleRow);
layout.add_content(ContentId::SensorDistanceRow);
layout.add_content(ContentId::LightnessSlider);
let first_param = layout.first_of_category(|id| id.is_parameter());
let first_slider = layout.first_of_category(|id| id.is_slider());
assert_eq!(first_param, Some(1));
assert_eq!(first_slider, Some(3));
}
}