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,
}
}
}