use serde::{Deserialize, Serialize};
use crate::ViewNode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ZsAccordionItemId(pub u64);
impl ZsAccordionItemId {
pub const fn new(value: u64) -> Self {
Self(value)
}
}
impl From<u64> for ZsAccordionItemId {
fn from(value: u64) -> Self {
Self::new(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ZsAccordionMode {
Single { collapsible: bool },
Multiple,
}
impl ZsAccordionMode {
pub const fn single(collapsible: bool) -> Self {
Self::Single { collapsible }
}
pub const fn multiple() -> Self {
Self::Multiple
}
}
impl Default for ZsAccordionMode {
fn default() -> Self {
Self::Single { collapsible: true }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZsAccordionChange {
pub item: ZsAccordionItemId,
pub expanded: bool,
pub next_expanded: Vec<ZsAccordionItemId>,
}
#[derive(Debug, Clone)]
pub struct ZsAccordionItem<Msg> {
pub(crate) id: ZsAccordionItemId,
pub(crate) title: String,
pub(crate) content: ViewNode<Msg>,
pub(crate) enabled: bool,
}
impl<Msg> ZsAccordionItem<Msg> {
pub fn new(
id: impl Into<ZsAccordionItemId>,
title: impl Into<String>,
content: ViewNode<Msg>,
) -> Self {
Self {
id: id.into(),
title: title.into(),
content,
enabled: true,
}
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub const fn id(&self) -> ZsAccordionItemId {
self.id
}
pub fn title(&self) -> &str {
&self.title
}
}
pub(crate) fn accordion_expanded_ids(
item_ids: &[ZsAccordionItemId],
expanded: impl IntoIterator<Item = ZsAccordionItemId>,
mode: ZsAccordionMode,
) -> Vec<ZsAccordionItemId> {
let mut normalized = Vec::new();
for id in expanded {
if item_ids.contains(&id) && !normalized.contains(&id) {
normalized.push(id);
if matches!(mode, ZsAccordionMode::Single { .. }) {
break;
}
}
}
normalized
}
pub(crate) fn accordion_change(
current: &[ZsAccordionItemId],
item: ZsAccordionItemId,
mode: ZsAccordionMode,
) -> ZsAccordionChange {
let was_expanded = current.contains(&item);
let mut next_expanded = current.to_vec();
match mode {
ZsAccordionMode::Single { collapsible } => {
if was_expanded {
if collapsible {
next_expanded.clear();
}
} else {
next_expanded.clear();
next_expanded.push(item);
}
}
ZsAccordionMode::Multiple => {
if was_expanded {
next_expanded.retain(|candidate| *candidate != item);
} else {
next_expanded.push(item);
}
}
}
ZsAccordionChange {
item,
expanded: next_expanded.contains(&item),
next_expanded,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_and_multiple_modes_produce_complete_next_state() {
let one = ZsAccordionItemId::new(1);
let two = ZsAccordionItemId::new(2);
let single = accordion_change(&[one], two, ZsAccordionMode::single(false));
assert_eq!(single.next_expanded, vec![two]);
let retained = accordion_change(&[two], two, ZsAccordionMode::single(false));
assert_eq!(retained.next_expanded, vec![two]);
let collapsed = accordion_change(&[two], two, ZsAccordionMode::single(true));
assert!(collapsed.next_expanded.is_empty());
let multiple = accordion_change(&[one], two, ZsAccordionMode::multiple());
assert_eq!(multiple.next_expanded, vec![one, two]);
let toggled = accordion_change(&multiple.next_expanded, one, ZsAccordionMode::multiple());
assert_eq!(toggled.next_expanded, vec![two]);
}
#[test]
fn normalization_rejects_unknown_duplicates_and_extra_single_values() {
let one = ZsAccordionItemId::new(1);
let two = ZsAccordionItemId::new(2);
let unknown = ZsAccordionItemId::new(99);
assert_eq!(
accordion_expanded_ids(
&[one, two],
[unknown, two, two, one],
ZsAccordionMode::multiple()
),
vec![two, one]
);
assert_eq!(
accordion_expanded_ids(&[one, two], [two, one], ZsAccordionMode::single(true)),
vec![two]
);
}
}