use crate::editor::panels::registry;
const APP_NAME: &str = "Concinnity";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum MenuCommand {
PanelToggle(usize),
Quit,
}
const QUIT_TAG: isize = 1;
const PANEL_TAG_BASE: isize = 0x100;
impl MenuCommand {
pub(super) fn tag(self) -> isize {
match self {
MenuCommand::Quit => QUIT_TAG,
MenuCommand::PanelToggle(i) => PANEL_TAG_BASE + i as isize,
}
}
pub(super) fn from_tag(tag: isize) -> Option<Self> {
match tag {
QUIT_TAG => Some(MenuCommand::Quit),
t if t >= PANEL_TAG_BASE => {
Some(MenuCommand::PanelToggle((t - PANEL_TAG_BASE) as usize))
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct PanelMarks(u32);
impl PanelMarks {
pub(crate) fn from_open(open: impl Iterator<Item = bool>) -> Self {
Self(
open.take(u32::BITS as usize)
.enumerate()
.filter(|(_, open)| *open)
.map(|(i, _)| 1 << i)
.sum(),
)
}
fn contains(self, i: usize) -> bool {
i < u32::BITS as usize && self.0 & (1 << i) != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Entry {
Separator,
Item(ItemSpec),
}
impl Entry {
pub(super) fn item(&self) -> Option<&ItemSpec> {
match self {
Entry::Separator => None,
Entry::Item(item) => Some(item),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ItemKind {
Standard(&'static str),
Command(MenuCommand),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct KeyEquivalent {
pub key: &'static str,
pub option: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ItemSpec {
pub title: String,
pub kind: ItemKind,
pub checked: bool,
pub key: Option<KeyEquivalent>,
}
fn standard(title: String, selector: &'static str, key: Option<KeyEquivalent>) -> Entry {
Entry::Item(ItemSpec {
title,
kind: ItemKind::Standard(selector),
checked: false,
key,
})
}
fn command(title: String, command: MenuCommand, checked: bool) -> Entry {
Entry::Item(ItemSpec {
title,
kind: ItemKind::Command(command),
checked,
key: None,
})
}
pub(super) fn app_entries() -> Vec<Entry> {
vec![
standard(
format!("About {APP_NAME}"),
"orderFrontStandardAboutPanel:",
None,
),
Entry::Separator,
standard(
format!("Hide {APP_NAME}"),
"hide:",
Some(KeyEquivalent {
key: "h",
option: false,
}),
),
standard(
"Hide Others".to_string(),
"hideOtherApplications:",
Some(KeyEquivalent {
key: "h",
option: true,
}),
),
standard("Show All".to_string(), "unhideAllApplications:", None),
Entry::Separator,
Entry::Item(ItemSpec {
title: format!("Quit {APP_NAME}"),
kind: ItemKind::Command(MenuCommand::Quit),
checked: false,
key: Some(KeyEquivalent {
key: "q",
option: false,
}),
}),
]
}
pub(super) fn view_entries(marks: PanelMarks) -> Vec<Entry> {
registry::VIEW_ROWS
.iter()
.enumerate()
.map(|(i, &(_, caption))| {
command(
caption.to_string(),
MenuCommand::PanelToggle(i),
marks.contains(i),
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn commands(entries: &[Entry]) -> Vec<MenuCommand> {
entries
.iter()
.filter_map(|e| match e.item()?.kind {
ItemKind::Command(c) => Some(c),
_ => None,
})
.collect()
}
fn all_open(open: bool) -> PanelMarks {
PanelMarks::from_open(std::iter::repeat_n(open, registry::VIEW_ROWS.len()))
}
#[test]
fn view_entries_are_the_view_panels_rows() {
let entries = view_entries(PanelMarks::default());
let titles: Vec<_> = entries
.iter()
.filter_map(Entry::item)
.map(|i| i.title.as_str())
.collect();
let expected: Vec<_> = registry::VIEW_ROWS
.iter()
.map(|&(_, caption)| caption)
.collect();
assert_eq!(titles, expected);
assert_eq!(
commands(&entries),
(0..expected.len())
.map(MenuCommand::PanelToggle)
.collect::<Vec<_>>()
);
}
#[test]
fn every_toggleable_panel_fits_the_marks() {
assert!(registry::VIEW_ROWS.len() <= u32::BITS as usize);
assert_ne!(all_open(true), all_open(false));
}
#[test]
fn a_row_is_marked_from_its_own_panel() {
let count = registry::VIEW_ROWS.len();
for open in 0..count {
let marks = PanelMarks::from_open((0..count).map(|i| i == open));
let marked: Vec<_> = view_entries(marks)
.into_iter()
.filter_map(|e| e.item().filter(|i| i.checked).map(|i| i.title.clone()))
.collect();
let (_, expected) = registry::VIEW_ROWS[open];
assert_eq!(marked, vec![expected.to_string()]);
}
}
#[test]
fn the_marks_span_none_to_all() {
let none = view_entries(all_open(false));
let all = view_entries(all_open(true));
assert!(none.iter().filter_map(Entry::item).all(|i| !i.checked));
assert!(all.iter().filter_map(Entry::item).all(|i| i.checked));
}
#[test]
fn every_command_survives_its_tag() {
let mut all = vec![MenuCommand::Quit];
all.extend((0..registry::VIEW_ROWS.len()).map(MenuCommand::PanelToggle));
for command in all {
assert_eq!(MenuCommand::from_tag(command.tag()), Some(command));
}
}
#[test]
fn a_tagless_item_is_no_command() {
assert_eq!(MenuCommand::from_tag(0), None);
}
#[test]
fn quit_is_the_only_app_entry_the_editor_answers() {
let entries = app_entries();
assert_eq!(commands(&entries), vec![MenuCommand::Quit]);
let last = entries.last().expect("an entry").item().expect("an item");
assert_eq!(last.kind, ItemKind::Command(MenuCommand::Quit));
}
}