mottes-floem-components 0.1.0

Some styled components for floem.
Documentation
//! # Views
//!
//! Components that were implemented as views. You can use all of those components like
//! you would use floem views :)

use std::fmt::Display;

use crate::{
    classes::{construct_theme, Label, RadioButton as RadioButtonComponent},
    parse_color,
    theme::Theme,
    BORDER_RADIUS,
};
use floem::{
    event::EventPropagation,
    peniko::Color,
    prelude::{SignalGet, SignalUpdate},
    taffy::AlignItems,
    unit::Pct,
    views::{
        clip, container, empty, img, label, slider::Slider, svg, v_stack, Checkbox, CheckboxClass,
        Clip, Container, Decorators, LabelClass, RadioButton, RadioButtonClass,
    },
    IntoView,
};

// Icons taken from lucide.dev
const CHEVRON_DOWN: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>"#;
const CHEVRON_RIGHT: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>"#;

/// A circular avatar image.
///
/// # Examples
///
/// ```no_run
/// # use mottes_floem_components::theme::Theme;
/// # use mottes_floem_components::views::avatar;
/// avatar(|| std::fs::read("examples/silly_robot_avatar.png").unwrap(), 50, Theme::dark());
/// ```
pub fn avatar(image: impl Fn() -> Vec<u8> + 'static, size: i32, theme: Theme<'static>) -> Clip {
    clip(img(image).style(move |s| s.width(size).height(size))).style(move |s| {
        s.border_radius(100)
            .width(size)
            .height(size)
            .border(1)
            .border_color(parse_color(theme.base00))
    })
}

/// A checkbox. Constructed using [`Checkbox::new_rw`](https://docs.rs/floem/latest/floem/views/struct.Checkbox.html#method.new_rw).
pub fn checkbox(
    checked: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static,
    theme: Theme<'static>,
) -> impl IntoView {
    Checkbox::new_rw(checked).style(move |s| {
        s.border_radius(BORDER_RADIUS)
            .border_color(parse_color(theme.base03))
            .background(parse_color(theme.base00))
            .color(parse_color(theme.base01))
            .hover(|s| {
                s.apply_if(checked.get(), |s| s.background(parse_color(theme.base0a)))
                    .apply_if(!checked.get(), |s| s.background(parse_color(theme.base00)))
            })
            .focus(|s| {
                s.hover(|s| {
                    s.apply_if(checked.get(), |s| s.background(parse_color(theme.base0a)))
                        .apply_if(!checked.get(), |s| s.background(parse_color(theme.base00)))
                })
                .border_color(parse_color(theme.base0a))
            })
            .disabled(|s| {
                s.color(parse_color(theme.base05).multiply_alpha(0.6))
                    .background(parse_color(theme.base03).multiply_alpha(0.6))
            })
            .active(|s| s.background(parse_color(theme.base0a)))
            .apply_if(checked.get(), |s| s.background(parse_color(theme.base0a)))
    })
}

/// A labeled checkbox. Constructed using
/// [`Checkbox::labeled_rw`](https://docs.rs/floem/latest/floem/views/struct.Checkbox.html#method.labeled_rw).
pub fn labeled_checkbox<S: Display + 'static>(
    checked: impl SignalGet<bool> + SignalUpdate<bool> + Copy + 'static,
    label: impl Fn() -> S + 'static,
    theme: Theme<'static>,
) -> impl IntoView {
    Checkbox::labeled_rw(checked, label).style(move |s| {
        s.class(CheckboxClass, |s| {
            s.border_radius(BORDER_RADIUS)
                .border_color(parse_color(theme.base03))
                .background(parse_color(theme.base00))
                .color(parse_color(theme.base01))
                .hover(|s| {
                    s.apply_if(checked.get(), |s| s.background(parse_color(theme.base0a)))
                        .apply_if(!checked.get(), |s| s.background(parse_color(theme.base00)))
                })
                .focus(|s| {
                    s.hover(|s| {
                        s.apply_if(checked.get(), |s| s.background(parse_color(theme.base0a)))
                            .apply_if(!checked.get(), |s| s.background(parse_color(theme.base00)))
                    })
                    .border_color(parse_color(theme.base0a))
                })
                .disabled(|s| {
                    s.color(parse_color(theme.base05).multiply_alpha(0.6))
                        .background(parse_color(theme.base03).multiply_alpha(0.6))
                })
                .active(|s| s.background(parse_color(theme.base0a)))
                .apply_if(checked.get(), |s| s.background(parse_color(theme.base0a)))
        })
        .class(LabelClass, |s| s.color(parse_color(theme.base05)))
        .hover(|s| s.background(parse_color(theme.base01)))
        .active(|s| s.class(CheckboxClass, |s| s.background(parse_color(theme.base01))))
    })
}

/// A labeled radio button. Constructed using
/// [`RadioButton::new_labeled_rw`](https://docs.rs/floem/latest/floem/views/struct.RadioButton.html#method.new_labeled_rw).
pub fn labeled_radio_button<T, S>(
    represented_value: T,
    actual_value: impl SignalGet<T> + SignalUpdate<T> + Copy + 'static,
    label: impl Fn() -> S + 'static,
    theme: Theme<'static>,
) -> impl IntoView
where
    S: Display + 'static,
    T: Eq + PartialEq + Clone + 'static,
{
    RadioButton::new_labeled_rw(represented_value, actual_value, label).style(move |_| {
        construct_theme(theme)
            .apply_class(RadioButtonComponent)
            .class(LabelClass, |s| s.color(parse_color(theme.base05)))
            .hover(|s| s.background(parse_color(theme.base01)))
            .focus(|s| {
                s.hover(|s| s.background(parse_color(theme.base01)))
                    .class(RadioButtonClass, |s| {
                        s.border_color(parse_color(theme.base0a))
                    })
            })
            .active(|s| {
                s.class(RadioButtonClass, |s| {
                    s.background(parse_color(theme.base01))
                        .border_color(parse_color(theme.base0a))
                })
            })
    })
}

/// A slider. Constructed using
/// [`Slider::new_rw`](https://docs.rs/floem/latest/floem/views/slider/struct.Slider.html#method.new_rw).
pub fn slider(
    percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static,
    theme: Theme<'static>,
) -> Slider {
    Slider::new_rw(percent)
        .slider_style(|s| {
            s.bar_color(parse_color(theme.base02))
                .accent_bar_color(parse_color(theme.base0a))
                .handle_color(Some(parse_color(theme.base03).into()))
                .accent_bar_radius(0)
        })
        .style(|s| {
            s.focus_visible(|s| {
                s.border(0.5)
                    .border_color(parse_color(theme.base0a))
                    .border_radius(100)
            })
        })
}

/// A progress bar.
///
/// # Examples
///
/// ```no_run
/// # use mottes_floem_components::views::progress_bar;
/// # use mottes_floem_components::theme::Theme;
/// use floem::reactive::create_rw_signal;
/// use floem::unit::Pct;
///
/// let progress_bar_pct = create_rw_signal(Pct::from(50.0));
/// progress_bar(progress_bar_pct, Theme::dark());
/// ```
pub fn progress_bar(
    percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static,
    theme: Theme<'static>,
) -> Slider {
    Slider::new_rw(percent)
        .slider_style(|s| {
            s.bar_color(parse_color(theme.base03))
                .accent_bar_color(parse_color(theme.base0a))
                .handle_color(Some(Color::TRANSPARENT.into()))
        })
        .disabled(|| true)
}

/// A solid line you can use to separate content.
pub fn separator(theme: Theme<'static>) -> Container {
    container(empty()).style(|s| {
        s.border(1)
            .border_color(parse_color(theme.base03))
            .border_radius(BORDER_RADIUS)
    })
}

/// An accordion item.
///
/// # Example
///
/// ```no_run
/// # use mottes_floem_components::theme::Theme;
/// # use mottes_floem_components::views::accordion_item;
/// use floem::reactive::create_rw_signal;
/// use floem::views::{v_stack, label};
///
/// let active_id = create_rw_signal(0);
/// let accordion = v_stack((
///   accordion_item(
///       label(|| "I'm some text"),
///       || "Accordion item 0",
///       0,
///       active_id,
///       Theme::dark(),
///   ),
///   accordion_item(
///       label(|| "I'm some more text"),
///       || "Accordion item 1",
///       1,
///       active_id,
///       Theme::dark(),
///   ),
///   accordion_item(
///       label(|| "Fuck facists, btw"),
///       || "Accordion item 2",
///       2,
///       active_id,
///       Theme::dark(),
///   )
/// ));
/// ```
pub fn accordion_item<V, S>(
    child: V,
    text: impl Fn() -> S + 'static,
    id: usize,
    active_id: impl SignalGet<usize> + SignalUpdate<usize> + Copy + 'static,
    theme: Theme<'static>,
) -> impl IntoView
where
    V: IntoView + 'static,
    S: Display + 'static,
{
    let chevron_svg = svg(CHEVRON_RIGHT)
        .style(|s| s.align_self(Some(AlignItems::End)).height(15).width(15))
        .class(Label)
        .update_value(move || {
            if active_id.get() == id {
                CHEVRON_DOWN
            } else {
                CHEVRON_RIGHT
            }
        });

    v_stack((
        container((
            label(text)
                .class(Label)
                .style(|s| s.align_self(Some(AlignItems::Start)).font_bold()),
            chevron_svg,
        )),
        child.style(move |s| s.apply_if(active_id.get() != id, |s| s.hide())),
    ))
    .on_click(move |_| {
        active_id.set(id);
        EventPropagation::Stop
    })
    .keyboard_navigable()
    .style(move |_| {
        construct_theme(theme)
            .gap(5)
            .width_full()
            .focus_visible(|s| s.border(1).border_color(parse_color(theme.base0a)))
    })
}