kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use crate::components::input_state::InputState;
use crate::layout::VStack;
use kael::prelude::FluentBuilder as _;
use kael::*;
use std::collections::{HashMap, HashSet};

pub struct FormState {
    fields: Vec<(SharedString, Entity<InputState>)>,
    errors: HashMap<SharedString, Vec<String>>,
    dirty: HashSet<SharedString>,
    submitted: bool,
}

impl FormState {
    pub fn new(cx: &mut App) -> Entity<Self> {
        cx.new(|_cx| Self {
            fields: Vec::new(),
            errors: HashMap::new(),
            dirty: HashSet::new(),
            submitted: false,
        })
    }

    pub fn register_field(&mut self, name: impl Into<SharedString>, state: Entity<InputState>) {
        let name = name.into();
        self.fields.push((name, state));
    }

    pub fn validate_all(&mut self, cx: &mut Context<Self>) -> bool {
        self.errors.clear();
        self.submitted = true;

        let fields: Vec<_> = self.fields.clone();
        let mut all_valid = true;

        for (name, field_entity) in fields {
            let result = field_entity.update(cx, |state, cx| state.validate(cx));

            if let Err(error) = result {
                all_valid = false;
                self.errors
                    .entry(name)
                    .or_default()
                    .push(error.message.to_string());
            }
        }

        cx.notify();
        all_valid
    }

    pub fn field_errors(&self, name: &str) -> Option<&Vec<String>> {
        self.errors.get(name)
    }

    pub fn is_valid(&self) -> bool {
        self.errors.is_empty()
    }

    pub fn is_dirty(&self) -> bool {
        !self.dirty.is_empty()
    }

    pub fn mark_dirty(&mut self, name: impl Into<SharedString>) {
        self.dirty.insert(name.into());
    }

    pub fn reset(&mut self, cx: &mut Context<Self>) {
        self.errors.clear();
        self.dirty.clear();
        self.submitted = false;
        cx.notify();
    }

    pub fn is_submitted(&self) -> bool {
        self.submitted
    }
}

#[derive(IntoElement)]
pub struct Form {
    state: Entity<FormState>,
    on_submit: Option<Box<dyn Fn(&mut Window, &mut App)>>,
    children: Vec<AnyElement>,
    style: StyleRefinement,
}

impl Form {
    pub fn new(state: Entity<FormState>) -> Self {
        Self {
            state,
            on_submit: None,
            children: Vec::new(),
            style: StyleRefinement::default(),
        }
    }

    pub fn on_submit(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
        self.on_submit = Some(Box::new(handler));
        self
    }

    pub fn child(mut self, child: impl IntoElement) -> Self {
        self.children.push(child.into_any_element());
        self
    }

    pub fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self {
        self.children
            .extend(children.into_iter().map(|c| c.into_any_element()));
        self
    }
}

impl Styled for Form {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

impl RenderOnce for Form {
    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
        let state = self.state.clone();
        let on_submit = self.on_submit;
        let user_style = self.style;

        VStack::new()
            .w_full()
            .gap(px(16.0))
            .children(self.children)
            .when_some(on_submit, |this, handler| {
                let handler = std::rc::Rc::new(handler);
                this.on_key_down(
                    move |event: &KeyDownEvent, window: &mut Window, cx: &mut App| {
                        if event.keystroke.key == "enter" {
                            let is_valid =
                                state.update(cx, |form_state, cx| form_state.validate_all(cx));
                            if is_valid {
                                (handler)(window, cx);
                            }
                        }
                    },
                )
            })
            .map(|this| {
                let mut vstack = this;
                vstack.style().refine(&user_style);
                vstack
            })
    }
}