vizia_core 0.4.0

Core components of vizia
use std::ops::Deref;

use crate::prelude::*;

pub enum AccordionEvent {
    ToggleOpen(usize, bool),
    ClearHeaders,
    RegisterHeader(usize, Entity),
    FocusNextHeader,
    FocusPrevHeader,
    FocusFirstHeader,
    FocusLastHeader,
}

/// A view which organizes content into expandable sections.
///
/// The accordion is implemented using internal [Collapsible] views and is fully
/// controlled by external open state passed to [`Handle::open`].
pub struct Accordion {
    open_indices: Signal<Vec<usize>>,
    on_toggle: Option<Box<dyn Fn(&mut EventContext, usize, bool) + 'static>>,
    header_entities: Vec<Entity>,
}

impl Accordion {
    /// Creates a new [Accordion] view.
    pub fn new<S, V, T, F>(cx: &mut Context, list: S, content: F) -> Handle<Self>
    where
        S: Res<V> + 'static,
        V: Deref<Target = [T]> + Clone + 'static,
        T: Clone + 'static,
        F: 'static + Clone + Fn(&mut Context, usize, T) -> AccordionPair,
    {
        let list = list.to_signal(cx);
        let open_indices = Signal::new(Vec::new());

        Self { open_indices, on_toggle: None, header_entities: Vec::new() }.build(cx, move |cx| {
            Keymap::from(vec![
                (
                    KeyChord::new(Modifiers::empty(), Code::ArrowDown),
                    KeymapEntry::new("Accordion Focus Next", |cx| {
                        cx.emit(AccordionEvent::FocusNextHeader)
                    }),
                ),
                (
                    KeyChord::new(Modifiers::empty(), Code::ArrowUp),
                    KeymapEntry::new("Accordion Focus Previous", |cx| {
                        cx.emit(AccordionEvent::FocusPrevHeader)
                    }),
                ),
                (
                    KeyChord::new(Modifiers::empty(), Code::Home),
                    KeymapEntry::new("Accordion Focus First", |cx| {
                        cx.emit(AccordionEvent::FocusFirstHeader)
                    }),
                ),
                (
                    KeyChord::new(Modifiers::empty(), Code::End),
                    KeymapEntry::new("Accordion Focus Last", |cx| {
                        cx.emit(AccordionEvent::FocusLastHeader)
                    }),
                ),
            ])
            .build(cx);

            Binding::new(cx, list, move |cx| {
                let list_values = list.get();
                let content = content.clone();
                let list_length = list.with(|list| list.len());

                cx.emit(AccordionEvent::ClearHeaders);

                for (index, item) in list_values.iter().cloned().enumerate() {
                    let pair = (content)(cx, index, item);

                    Collapsible::new(cx, pair.header, pair.content)
                        .on_build(move |cx| {
                            if let Some(header) = cx.nth_child(0) {
                                cx.emit(AccordionEvent::RegisterHeader(index, header));
                            }
                        })
                        .on_toggle(move |cx, next_open| {
                            cx.emit(AccordionEvent::ToggleOpen(index, next_open));
                        })
                        .open(open_indices.map(move |indices| indices.contains(&index)));

                    if index < list_length - 1 {
                        Divider::horizontal(cx);
                    }
                }
            });
        })
    }
}

impl View for Accordion {
    fn element(&self) -> Option<&'static str> {
        Some("accordion")
    }

    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|accordion_event, _| match accordion_event {
            AccordionEvent::ToggleOpen(index, next_open) => {
                if let Some(callback) = &self.on_toggle {
                    (callback)(cx, *index, *next_open);
                }
            }

            AccordionEvent::ClearHeaders => {
                self.header_entities.clear();
            }

            AccordionEvent::RegisterHeader(index, entity) => {
                if self.header_entities.len() <= *index {
                    self.header_entities.resize(*index + 1, Entity::null());
                }

                self.header_entities[*index] = *entity;
            }

            AccordionEvent::FocusNextHeader => {
                if let Some(index) = self.focused_header_index(cx) {
                    let next_index = (index + 1) % self.header_entities.len();
                    let next_header = self.header_entities[next_index];
                    cx.with_current(next_header, |cx| cx.focus());
                }
            }

            AccordionEvent::FocusPrevHeader => {
                if let Some(index) = self.focused_header_index(cx) {
                    let prev_index =
                        if index == 0 { self.header_entities.len() - 1 } else { index - 1 };
                    let prev_header = self.header_entities[prev_index];
                    cx.with_current(prev_header, |cx| cx.focus());
                }
            }

            AccordionEvent::FocusFirstHeader => {
                if let Some(first_header) = self.header_entities.first().copied() {
                    cx.with_current(first_header, |cx| cx.focus());
                }
            }

            AccordionEvent::FocusLastHeader => {
                if let Some(last_header) = self.header_entities.last().copied() {
                    cx.with_current(last_header, |cx| cx.focus());
                }
            }
        });

        event.map(|window_event, meta| match window_event {
            WindowEvent::KeyDown(code, _) => match code {
                Code::ArrowDown | Code::ArrowUp | Code::Home | Code::End => {
                    if self.focused_header_index(cx).is_some() {
                        meta.consume();
                    }
                }
                _ => {}
            },
            _ => {}
        });
    }
}

impl Accordion {
    fn focused_header_index(&self, cx: &EventContext) -> Option<usize> {
        let focused = cx.focused();
        self.header_entities.iter().position(|header| {
            !header.is_null() && (focused == *header || focused.is_descendant_of(cx.tree, *header))
        })
    }
}

impl Handle<'_, Accordion> {
    /// Sets which sections are open by index.
    pub fn open(mut self, indices: impl Res<Vec<usize>> + 'static) -> Self {
        let indices = indices.to_signal(self.context());
        self.bind(indices, move |handle| {
            handle.modify(|accordion| {
                accordion.open_indices.set(indices.get());
            });
        })
    }

    /// Set a callback that fires when a section is toggled open or closed.
    ///
    /// The callback receives the section index and the desired next open state.
    /// Use this to update the external signal passed to `open()`.
    pub fn on_toggle<F>(self, callback: F) -> Self
    where
        F: 'static + Fn(&mut EventContext, usize, bool),
    {
        self.modify(|accordion| {
            accordion.on_toggle = Some(Box::new(callback));
        })
    }
}

pub struct AccordionPair {
    pub header: Box<dyn Fn(&mut Context)>,
    pub content: Box<dyn Fn(&mut Context)>,
}

impl AccordionPair {
    pub fn new<H, C>(header: H, content: C) -> Self
    where
        H: 'static + Fn(&mut Context),
        C: 'static + Fn(&mut Context),
    {
        Self { header: Box::new(header), content: Box::new(content) }
    }
}