rae 0.1.18

Renderer-neutral Rust design catalog for desktop widgets, layouts, sanitized output, and GLSL shader primitives.
Documentation
use iced::widget::{button, column, container, row, scrollable, text, text_input};
use iced::{alignment, border, Border, Center, Color, Element, Fill, Length, Size, Task, Theme};
use rae::{
    built_in_widget_catalog, WidgetCatalog, WidgetCatalogQuery, WidgetDescriptor, WidgetUseCase,
};

pub fn main() -> iced::Result {
    iced::application(
        "rae catalog browser",
        CatalogBrowser::update,
        CatalogBrowser::view,
    )
    .theme(CatalogBrowser::theme)
    .window_size(Size::new(1180.0, 760.0))
    .run_with(|| (CatalogBrowser::new(), Task::none()))
}

struct CatalogBrowser {
    catalog: WidgetCatalog,
    query: String,
    selected_id: String,
    use_case: Option<WidgetUseCase>,
}

#[derive(Debug, Clone)]
enum Message {
    QueryChanged(String),
    QuickSearch(&'static str),
    UseCase(Option<WidgetUseCase>),
    Select(String),
}

impl CatalogBrowser {
    fn new() -> Self {
        let catalog = built_in_widget_catalog();
        let selected_id = catalog
            .widgets
            .first()
            .map(|widget| widget.id.clone())
            .unwrap_or_else(|| "button".to_string());
        Self {
            catalog,
            query: String::new(),
            selected_id,
            use_case: None,
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::QueryChanged(query) => {
                self.query = query;
                self.select_first_result();
            }
            Message::QuickSearch(query) => {
                self.query = query.to_string();
                self.select_first_result();
            }
            Message::UseCase(use_case) => {
                self.use_case = use_case;
                self.select_first_result();
            }
            Message::Select(id) => self.selected_id = id,
        }
    }

    fn view(&self) -> Element<'_, Message> {
        let summary = self.catalog.summary();
        let shell = column![
            row![
                text("Rae Catalog Browser").size(30),
                metric("widgets", summary.widget_count.to_string()),
                metric("examples", summary.example_count.to_string()),
                metric("tokens", summary.token_preset_count.to_string()),
                metric("recipes", summary.recipe_kind_count.to_string()),
            ]
            .spacing(14)
            .align_y(Center),
            row![
                text_input("Search catalog", &self.query)
                    .on_input(Message::QueryChanged)
                    .padding(10)
                    .width(Fill),
                button("All").on_press(Message::UseCase(None)),
                button("AI").on_press(Message::UseCase(Some(WidgetUseCase::AiWorkspace))),
                button("Code").on_press(Message::UseCase(Some(WidgetUseCase::DeveloperTools))),
                button("Audio").on_press(Message::UseCase(Some(WidgetUseCase::AudioProduction))),
                button("Creative").on_press(Message::UseCase(Some(WidgetUseCase::CreativeTools))),
            ]
            .spacing(10)
            .align_y(Center),
            row![
                button("cupertino").on_press(Message::QuickSearch("cupertino")),
                button("agent").on_press(Message::QuickSearch("agent")),
                button("terminal").on_press(Message::QuickSearch("terminal")),
                button("browser").on_press(Message::QuickSearch("browser")),
                button("automation").on_press(Message::QuickSearch("automation")),
                button("overlay").on_press(Message::QuickSearch("overlay")),
            ]
            .spacing(8),
            row![self.result_list(), self.detail_panel()]
                .spacing(14)
                .height(Fill),
        ]
        .padding(18)
        .spacing(14);

        container(shell)
            .width(Fill)
            .height(Fill)
            .style(style::desktop)
            .into()
    }

    fn result_list(&self) -> Element<'_, Message> {
        let mut rows = column![].spacing(8);
        for widget in self.results() {
            rows = rows.push(self.widget_row(widget));
        }

        container(scrollable(rows).height(Fill))
            .width(Length::FillPortion(2))
            .height(Fill)
            .padding(12)
            .style(style::panel)
            .into()
    }

    fn detail_panel(&self) -> Element<'_, Message> {
        let selected = self
            .catalog
            .find(&self.selected_id)
            .or_else(|| self.catalog.widgets.first());
        let Some(widget) = selected else {
            return container(text("Catalog is empty")).into();
        };

        let mut examples = column![].spacing(8);
        for example in widget.examples.iter().take(12) {
            examples = examples.push(
                container(
                    row![
                        text(example.title.clone()).size(13).width(Fill),
                        text(example.id.clone()).size(10),
                    ]
                    .spacing(8)
                    .align_y(Center),
                )
                .padding([8, 10])
                .style(style::example),
            );
        }

        container(
            column![
                text(widget.name.clone()).size(26),
                text(widget.id.clone()).size(12),
                text(widget.summary.clone()).size(14),
                row![
                    pill(widget.category.label()),
                    pill(widget.complexity.label()),
                    pill(format!("{} examples", widget.examples.len())),
                ]
                .spacing(8),
                text("Use Cases").size(12),
                text(
                    widget
                        .use_cases
                        .iter()
                        .map(|use_case| use_case.label())
                        .collect::<Vec<_>>()
                        .join(" · ")
                )
                .size(13),
                text("Examples").size(12),
                scrollable(examples).height(Fill),
            ]
            .spacing(12),
        )
        .width(Length::FillPortion(3))
        .height(Fill)
        .padding(16)
        .style(style::panel)
        .into()
    }

    fn widget_row<'a>(&'a self, widget: &'a WidgetDescriptor) -> Element<'a, Message> {
        let selected = self.selected_id == widget.id;
        let id = widget.id.clone();
        button(
            container(
                column![
                    row![
                        text(widget.name.clone()).size(15).width(Fill),
                        text(widget.category.label()).size(10),
                    ]
                    .spacing(8)
                    .align_y(Center),
                    text(widget.summary.clone()).size(12),
                    row![
                        pill(widget.complexity.label()),
                        pill(format!("{} examples", widget.examples.len())),
                    ]
                    .spacing(6),
                ]
                .spacing(7),
            )
            .padding(12)
            .style(move |_| style::row_card(selected)),
        )
        .style(button::text)
        .on_press(Message::Select(id))
        .into()
    }

    fn results(&self) -> Vec<&WidgetDescriptor> {
        let mut query = WidgetCatalogQuery::new().text(&self.query).limit(96);
        if let Some(use_case) = self.use_case {
            query = query.use_case(use_case);
        }
        self.catalog.query(query)
    }

    fn select_first_result(&mut self) {
        if let Some(first) = self.results().first() {
            self.selected_id = first.id.clone();
        }
    }

    fn theme(&self) -> Theme {
        Theme::Dark
    }
}

fn metric<'a>(label: &'a str, value: String) -> Element<'a, Message> {
    container(
        column![text(label).size(10), text(value).size(18)]
            .spacing(2)
            .align_x(alignment::Horizontal::Center),
    )
    .padding([8, 12])
    .style(style::metric)
    .into()
}

fn pill<'a>(label: impl Into<String>) -> Element<'a, Message> {
    container(text(label.into()).size(10))
        .padding([4, 8])
        .style(style::pill)
        .into()
}

mod style {
    use super::*;
    use iced::widget::container;

    pub fn desktop(_theme: &Theme) -> container::Style {
        container::Style {
            text_color: Some(rgba8(238, 244, 255, 255)),
            background: Some(rgba8(8, 10, 16, 255).into()),
            ..Default::default()
        }
    }

    pub fn panel(_theme: &Theme) -> container::Style {
        surface(rgba8(16, 21, 33, 236), rgba8(132, 158, 210, 92), 18.0)
    }

    pub fn metric(_theme: &Theme) -> container::Style {
        surface(rgba8(20, 27, 43, 238), rgba8(122, 185, 255, 116), 16.0)
    }

    pub fn example(_theme: &Theme) -> container::Style {
        surface(rgba8(10, 14, 22, 194), rgba8(122, 145, 190, 56), 12.0)
    }

    pub fn pill(_theme: &Theme) -> container::Style {
        surface(rgba8(34, 43, 64, 218), rgba8(142, 166, 214, 72), 99.0)
    }

    pub fn row_card(selected: bool) -> container::Style {
        if selected {
            surface(rgba8(36, 57, 96, 242), rgba8(122, 185, 255, 180), 16.0)
        } else {
            surface(rgba8(12, 17, 27, 214), rgba8(106, 126, 164, 54), 16.0)
        }
    }

    fn surface(background: Color, border_color: Color, radius: f32) -> container::Style {
        container::Style {
            text_color: Some(rgba8(238, 244, 255, 255)),
            background: Some(background.into()),
            border: Border {
                color: border_color,
                width: 1.0,
                radius: border::Radius::from(radius),
            },
            ..Default::default()
        }
    }

    fn rgba8(r: u8, g: u8, b: u8, a: u8) -> Color {
        Color {
            r: r as f32 / 255.0,
            g: g as f32 / 255.0,
            b: b as f32 / 255.0,
            a: a as f32 / 255.0,
        }
    }
}