schemaui 0.11.0

A Rust library for generating TUI and Web UIs from JSON Schemas for configuration management.
Documentation
use std::sync::Arc;

use jsonschema::Validator;

use crate::tui::state::FormState;
use crate::tui::state::field::components::helpers::OverlayContext;

use super::state::{
    CompositeOverlayTarget, EntryAdvance, EntryTabsKind, OverlayFocusMode, OverlayHost,
    OverlaySession, OverlayState, OverlayStore,
};

#[derive(Clone)]
pub(crate) struct CompositeEditorOverlay {
    pub(super) state: OverlayState,
    pub(super) store: OverlayStore,
}

impl CompositeEditorOverlay {
    pub(super) fn new(
        field_pointer: String,
        field_label: String,
        level: usize,
        host: OverlayHost,
        session: OverlaySession,
        instructions: String,
    ) -> Self {
        let display_title = format!("Edit {}{}", field_label, session.title());
        let display_description = session.description();
        let store = OverlayStore::new(display_title, display_description, instructions);
        let state = OverlayState::new(field_pointer, field_label, host, level, session);
        Self { state, store }
    }

    pub(super) fn build_commit_payload(&self) -> super::state::OverlayResult {
        self.state.build_result()
    }

    pub(super) fn needs_list_panel(&self) -> bool {
        matches!(
            self.state.target(),
            CompositeOverlayTarget::ListEntry { .. }
                | CompositeOverlayTarget::KeyValueEntry { .. }
                | CompositeOverlayTarget::ArrayEntry { .. }
        )
    }

    pub(crate) fn form_state(&self) -> &FormState {
        self.state.form_state()
    }

    pub(crate) fn form_state_mut(&mut self) -> &mut FormState {
        self.state.form_state_mut()
    }

    pub(super) fn dirty(&self) -> bool {
        self.state.dirty()
    }

    pub(super) fn can_focus_entries(&self) -> bool {
        self.store.can_focus_entries()
    }

    pub(super) fn focus_entries(&mut self) -> bool {
        if self.store.can_focus_entries() {
            self.store.focus_entries();
            true
        } else {
            false
        }
    }

    pub(super) fn focus_form_default(&mut self) -> bool {
        if !self.form_state().has_focusable_fields() {
            return false;
        }
        self.store.focus_form();
        true
    }

    pub(super) fn focus_form_first(&mut self) -> bool {
        if !self.focus_form_default() {
            return false;
        }
        if !self.form_state_mut().focus_first_field_with_layout() {
            self.form_state_mut().focus_first_field();
        }
        true
    }

    pub(super) fn focus_form_last(&mut self) -> bool {
        if !self.focus_form_default() {
            return false;
        }
        self.form_state_mut().focus_last_field();
        true
    }

    pub(super) fn apply_component_context(&mut self, ctx: OverlayContext) {
        let field_label = self.field_label().to_string();
        self.store.apply_component_context(&field_label, ctx);
    }

    pub(super) fn entry_tabs_len(&self) -> usize {
        self.store.entry_tabs_len()
    }

    pub(crate) fn entry_tabs_selected(&self) -> Option<usize> {
        self.store.entry_tabs_selected()
    }

    pub(crate) fn entry_tabs_entries(&self) -> Option<&[String]> {
        self.store.entry_tabs_entries()
    }

    pub(crate) fn entry_tabs_label(&self) -> Option<&str> {
        self.store.entry_tabs_label().map(|label| label.as_str())
    }

    pub(super) fn entry_tabs_snapshot(&self) -> Option<(usize, usize)> {
        self.store.entry_tabs_snapshot()
    }

    pub(super) fn set_entry_tabs(&mut self, entries: Vec<String>, selected: usize) {
        self.store.set_entry_tabs("Entries", entries, selected);
    }

    pub(super) fn set_variant_tabs(
        &mut self,
        entries: Vec<String>,
        ids: Vec<usize>,
        selected: usize,
    ) {
        self.store
            .set_variant_tabs("Variants", entries, ids, selected);
        self.store.append_instructions(
            "Ctrl+←/→ switches variants; Tab focuses variant tabs".to_string(),
        );
    }

    pub(crate) fn instructions(&self) -> &str {
        self.store.instructions()
    }

    pub(crate) fn title(&self) -> &str {
        self.store.title()
    }

    pub(crate) fn description(&self) -> Option<&String> {
        self.store.description()
    }

    pub(super) fn focus_mode(&self) -> OverlayFocusMode {
        self.store.focus()
    }

    pub(crate) fn field_label(&self) -> &str {
        self.state.field_label()
    }

    pub(crate) fn level(&self) -> usize {
        self.state.level()
    }

    pub(crate) fn host(&self) -> OverlayHost {
        self.state.host()
    }

    pub(crate) fn field_pointer(&self) -> &str {
        self.state.field_pointer()
    }

    pub(crate) fn target(&self) -> &CompositeOverlayTarget {
        self.state.target()
    }

    pub(super) fn target_mut(&mut self) -> &mut CompositeOverlayTarget {
        self.state.target_mut()
    }

    pub(super) fn set_target(&mut self, target: CompositeOverlayTarget) {
        self.state.set_target(target);
    }

    pub(super) fn exit_armed(&self) -> bool {
        self.state.exit_armed()
    }

    pub(super) fn set_exit_armed(&mut self, armed: bool) {
        self.state.set_exit_armed(armed);
    }

    pub(super) fn validator_clone(&self) -> Option<Arc<Validator>> {
        self.state.validator_clone()
    }

    pub(super) fn set_validator(&mut self, validator: Option<Arc<Validator>>) {
        self.state.set_validator(validator);
    }

    pub(super) fn session(&self) -> &OverlaySession {
        self.state.session()
    }

    pub(super) fn current_variant_index(&self) -> Option<usize> {
        self.state.current_variant_index()
    }

    pub(super) fn take_composite_session(
        &mut self,
    ) -> Option<crate::tui::state::CompositeEditorSession> {
        self.state.take_composite_session()
    }

    pub(super) fn replace_composite_session(
        &mut self,
        session: crate::tui::state::CompositeEditorSession,
    ) {
        self.state.replace_composite_session(session);
    }

    pub(super) fn sync_variant_selection(&mut self, variant_index: usize) {
        if let Some(store) = self.store.entry_tabs.as_mut() {
            store.select_by_id(variant_index);
        }
    }

    pub(super) fn advance_entry_tab(&mut self, delta: i32) -> Option<EntryAdvance> {
        let store = self.store.entry_tabs.as_mut()?;
        if !store.advance(delta) {
            return None;
        }
        match store.kind() {
            EntryTabsKind::Entries => Some(EntryAdvance::Collection {
                selected: store.selected(),
            }),
            EntryTabsKind::Variants => store
                .selected_id()
                .map(|variant_index| EntryAdvance::Variant { variant_index }),
        }
    }

    pub(super) fn session_kind(&self) -> &'static str {
        match self.session() {
            OverlaySession::Composite(_) => "composite",
            OverlaySession::KeyValue(_) => "keyvalue",
            OverlaySession::Array(_) => "array",
            OverlaySession::Detached => "detached",
        }
    }

    pub(super) fn validator_cache_key(&self) -> String {
        let target = match self.target() {
            CompositeOverlayTarget::Field => "field",
            CompositeOverlayTarget::ListEntry { .. } => "list",
            CompositeOverlayTarget::KeyValueEntry { .. } => "keyvalue-entry",
            CompositeOverlayTarget::ArrayEntry { .. } => "array-entry",
        };
        format!(
            "{}::{}::{}",
            self.field_pointer(),
            self.session_kind(),
            target
        )
    }

    pub(super) fn advance_focus(
        &mut self,
        direction: super::state::FocusDirection,
    ) -> super::state::FocusOutcome {
        use super::state::{FocusOutcome, OverlayFocusMode};

        let has_fields = self.form_state().has_focusable_fields();
        let entries_len = self.entry_tabs_len();
        let focus_mode = self.focus_mode();
        match direction {
            super::state::FocusDirection::Forward => {
                if focus_mode == OverlayFocusMode::EntryTabs {
                    if has_fields && self.focus_form_first() {
                        return FocusOutcome::Consumed;
                    }
                    if entries_len > 0 {
                        return FocusOutcome::RequestEntryDelta(1);
                    }
                    if self.focus_entries() {
                        return FocusOutcome::Consumed;
                    }
                    return FocusOutcome::Consumed;
                }
                if !has_fields {
                    if entries_len > 0 {
                        return FocusOutcome::RequestEntryDelta(1);
                    }
                    if self.focus_entries() {
                        return FocusOutcome::Consumed;
                    }
                    return FocusOutcome::Consumed;
                }
                if self.form_state().focus_is_last() {
                    if entries_len > 0 {
                        return FocusOutcome::RequestEntryDelta(1);
                    }
                    if self.focus_entries() {
                        return FocusOutcome::Consumed;
                    }
                    return FocusOutcome::Consumed;
                }
                FocusOutcome::PassThrough
            }
            super::state::FocusDirection::Backward => {
                if focus_mode == OverlayFocusMode::EntryTabs {
                    if has_fields && self.focus_form_last() {
                        return FocusOutcome::Consumed;
                    }
                    if entries_len > 0 {
                        return FocusOutcome::RequestEntryDelta(-1);
                    }
                    if self.focus_entries() {
                        return FocusOutcome::Consumed;
                    }
                    return FocusOutcome::Consumed;
                }
                if !has_fields {
                    if entries_len > 0 {
                        return FocusOutcome::RequestEntryDelta(-1);
                    }
                    if self.focus_entries() {
                        return FocusOutcome::Consumed;
                    }
                    return FocusOutcome::Consumed;
                }
                if self.form_state().focus_is_first() {
                    if entries_len > 0 {
                        return FocusOutcome::RequestEntryDelta(-1);
                    }
                    if self.focus_entries() {
                        return FocusOutcome::Consumed;
                    }
                    return FocusOutcome::Consumed;
                }
                FocusOutcome::PassThrough
            }
        }
    }
}