use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input};
use iced::{highlighter, keyboard};
use iced::{Border, Center, Color, Element, Fill, Font, Length, Size, Subscription, Task, Theme};
use rae::{
analyze_code_line, build_layout_recipe, built_in_widget_catalog, materialize_output,
AppRecipeKind, CommandItem, CommandPalette, ComponentRole, ComponentSize, DesignTokenSet,
OutputOptions, OutputRow, Rect, Rgba, Shortcut, SnippetSet, TextSnippet, WidgetCatalog,
WidgetCatalogQuery, WidgetCatalogSummary, WidgetDescriptor, WidgetGlowSurface,
WidgetLoaderKind, WidgetLoaderModel, WidgetLoaderSegment, WidgetState, WidgetUseCase,
WidgetVariant,
};
use std::sync::Arc;
pub fn main() -> iced::Result {
iced::application("rae code editor", CodeEditor::update, CodeEditor::view)
.theme(CodeEditor::theme)
.subscription(CodeEditor::subscription)
.window_size(Size::new(1280.0, 820.0))
.run_with(|| (CodeEditor::new(), Task::none()))
}
struct CodeEditor {
panes: pane_grid::State<EditorPane>,
focus: Option<pane_grid::Pane>,
editor: text_editor::Content,
active_file: SampleFile,
dirty: bool,
command_palette: CommandPalette,
snippets: SnippetSet,
snippet_query: String,
catalog: WidgetCatalog,
assistant_mode: AssistantMode,
show_metrics: bool,
}
#[derive(Debug, Clone)]
enum Message {
EditorAction(text_editor::Action),
OpenSample(SampleFile),
CommandQueryChanged(String),
SnippetQueryChanged(String),
RunCommand(String),
InsertSnippet(String),
PaneClicked(pane_grid::Pane),
PaneDragged(pane_grid::DragEvent),
PaneResized(pane_grid::ResizeEvent),
MaximizePane(pane_grid::Pane),
RestorePanes,
}
impl CodeEditor {
fn new() -> Self {
let (mut panes, editor) = pane_grid::State::new(EditorPane::Editor);
let (assistant, _) = panes
.split(pane_grid::Axis::Horizontal, editor, EditorPane::Assistant)
.expect("initial assistant pane split should fit");
let _ = panes.split(pane_grid::Axis::Vertical, editor, EditorPane::Files);
let _ = panes.split(pane_grid::Axis::Vertical, assistant, EditorPane::Inspector);
Self {
panes,
focus: Some(editor),
editor: text_editor::Content::with_text(SampleFile::Main.source()),
active_file: SampleFile::Main,
dirty: false,
command_palette: CommandPalette::new(command_items()),
snippets: SnippetSet::new(snippets()),
snippet_query: "component".to_string(),
catalog: built_in_widget_catalog(),
assistant_mode: AssistantMode::Review,
show_metrics: true,
}
}
fn update(&mut self, message: Message) {
match message {
Message::EditorAction(action) => {
self.dirty = self.dirty || action.is_edit();
self.editor.perform(action);
}
Message::OpenSample(file) => {
self.active_file = file;
self.editor = text_editor::Content::with_text(file.source());
self.dirty = false;
self.assistant_mode = AssistantMode::Review;
}
Message::CommandQueryChanged(query) => {
self.command_palette.set_query(query);
}
Message::SnippetQueryChanged(query) => {
self.snippet_query = query;
}
Message::RunCommand(id) => self.run_command(&id),
Message::InsertSnippet(trigger) => {
if let Some(body) = self.snippets.expand(&trigger) {
self.editor
.perform(text_editor::Action::Edit(text_editor::Edit::Paste(
Arc::new(body.to_string()),
)));
self.dirty = true;
self.assistant_mode = AssistantMode::Review;
}
}
Message::PaneClicked(pane) => {
self.focus = Some(pane);
}
Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => {
self.panes.drop(pane, target);
}
Message::PaneDragged(_) => {}
Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => {
self.panes.resize(split, ratio);
}
Message::MaximizePane(pane) => {
self.panes.maximize(pane);
}
Message::RestorePanes => {
self.panes.restore();
}
}
}
fn run_command(&mut self, id: &str) {
match id {
"format" => {
let formatted = format_source(&self.editor.text());
self.editor = text_editor::Content::with_text(&formatted);
self.dirty = true;
self.assistant_mode = AssistantMode::Formatted;
}
"review" => {
self.assistant_mode = AssistantMode::Review;
}
"catalog" => {
self.assistant_mode = AssistantMode::Catalog;
self.command_palette.set_query("catalog");
}
"diagnostics" => {
self.assistant_mode = AssistantMode::Diagnostics;
}
"design-board" => {
self.assistant_mode = AssistantMode::DesignBoard;
self.command_palette.set_query("design");
}
"metrics" => {
self.show_metrics = !self.show_metrics;
}
"insert-component" => {
if let Some(body) = self.snippets.expand("component") {
self.editor
.perform(text_editor::Action::Edit(text_editor::Edit::Paste(
Arc::new(body.to_string()),
)));
self.dirty = true;
}
}
"clear" => {
self.command_palette.set_query("");
self.snippet_query.clear();
}
_ => {}
}
}
fn view(&self) -> Element<'_, Message> {
let focus = self.focus;
let total_panes = self.panes.len();
let grid = PaneGrid::new(&self.panes, |id, pane, is_maximized| {
let is_focused = focus == Some(id);
let title = row![
text(pane.title()).size(16),
if is_focused {
text("active").size(13)
} else {
text("").size(13)
},
]
.spacing(8)
.align_y(Center);
let controls = view_pane_controls(id, total_panes, is_maximized);
let title_bar = pane_grid::TitleBar::new(title)
.controls(pane_grid::Controls::new(controls))
.padding(8)
.style(if is_focused {
app_style::title_bar_focused
} else {
app_style::title_bar_active
});
pane_grid::Content::new(self.view_pane(*pane))
.title_bar(title_bar)
.style(if is_focused {
app_style::pane_focused
} else {
app_style::pane_active
})
})
.width(Fill)
.height(Fill)
.spacing(10)
.on_click(Message::PaneClicked)
.on_drag(Message::PaneDragged)
.on_resize(10, Message::PaneResized);
container(column![self.view_header(), grid].spacing(12).height(Fill))
.padding(12)
.style(app_style::app_background)
.into()
}
fn view_header(&self) -> Element<'_, Message> {
let source = self.editor.text();
let block = code_block_analysis(&source);
let diagnostics = collect_diagnostics(&source);
let current_query = if self.command_palette.query.is_empty() {
"all widgets".to_string()
} else {
self.command_palette.query.clone()
};
let title = column![
text("Rae Studio").size(30),
text("desktop code workbench + renderer-neutral design catalog").size(13),
]
.spacing(2)
.width(Fill);
let stats = row![
metric_tile("buffer", self.active_file.kind()),
metric_tile("semantic", block.semantic_line_count.to_string()),
metric_tile("diagnostics", diagnostics.len().to_string()),
metric_tile("catalog", format!("{} items", self.catalog.widgets.len())),
]
.spacing(8)
.align_y(Center);
container(
column![
row![title, stats].spacing(16).align_y(Center),
row![
chip(format!("file: {}", self.active_file.title())),
chip(format!("query: {current_query}")),
chip(format!("mode: {}", self.assistant_mode.title())),
chip(if self.dirty { "modified" } else { "clean" }),
]
.spacing(8)
.align_y(Center),
]
.spacing(12),
)
.padding(16)
.style(app_style::hero)
.into()
}
fn view_pane(&self, pane: EditorPane) -> Element<'_, Message> {
match pane {
EditorPane::Files => self.view_files(),
EditorPane::Editor => self.view_editor(),
EditorPane::Assistant => self.view_assistant(),
EditorPane::Inspector => self.view_inspector(),
}
}
fn view_files(&self) -> Element<'_, Message> {
let mut file_list = column![].spacing(8);
for file in SampleFile::ALL {
let content = container(
row![
column![
text(file.title()).size(15),
text(file.description()).size(12),
]
.spacing(2)
.width(Fill),
chip(file.kind()),
]
.spacing(8)
.align_y(Center),
)
.width(Fill)
.padding(10)
.style(if file == self.active_file {
app_style::selected_card
} else {
app_style::interactive_card
});
let mut open = button(content)
.style(button::text)
.on_press(Message::OpenSample(file));
if file == self.active_file {
open = open.style(button::secondary);
}
file_list = file_list.push(open);
}
let files = column![
text("Workspace").size(24),
text("Open buffers and command surfaces").size(13),
section("Buffers", file_list.into()),
section("Commands", self.view_commands()),
section("Snippets", self.view_snippets()),
]
.spacing(12);
container(scrollable(files).height(Fill)).padding(10).into()
}
fn view_commands(&self) -> Element<'_, Message> {
let mut list = column![text_input("Search commands", &self.command_palette.query)
.on_input(Message::CommandQueryChanged)
.padding(8),]
.spacing(6);
for item in self.command_palette.filtered_items_limited(6) {
let mut label = column![text(&item.title).size(14)].spacing(2);
if let Some(subtitle) = &item.subtitle {
label = label.push(text(subtitle).size(12));
}
let mut card = row![label.width(Fill)].spacing(8).align_y(Center);
if let Some(shortcut) = &item.shortcut {
card = card.push(chip(shortcut.label()));
}
list = list.push(
button(
container(card)
.padding(8)
.width(Fill)
.style(app_style::interactive_card),
)
.style(button::text)
.on_press(Message::RunCommand(item.id.clone())),
);
}
list.into()
}
fn view_snippets(&self) -> Element<'_, Message> {
let mut list = column![text_input("Search snippets", &self.snippet_query)
.on_input(Message::SnippetQueryChanged)
.padding(8),]
.spacing(6);
for snippet in self.snippets.matches_limited(&self.snippet_query, 5) {
let title = snippet.description.as_deref().unwrap_or(&snippet.trigger);
list = list.push(
button(
container(
column![text(&snippet.trigger).size(13), text(title).size(11)].spacing(1),
)
.padding(8)
.width(Fill)
.style(app_style::interactive_card),
)
.style(button::text)
.on_press(Message::InsertSnippet(snippet.trigger.clone())),
);
}
list.into()
}
fn view_editor(&self) -> Element<'_, Message> {
let (line, column_index) = self.editor.cursor_position();
let dirty = if self.dirty { "modified" } else { "saved" };
let toolbar = container(
row![
column![
text(self.active_file.title()).size(18),
text(self.active_file.description()).size(12),
]
.spacing(2)
.width(Fill),
chip(format!("{} lines", self.editor.line_count())),
chip(format!("{}:{}", line + 1, column_index + 1)),
chip(dirty),
button("Format")
.style(button::secondary)
.on_press(Message::RunCommand("format".to_string())),
button("Review").on_press(Message::RunCommand("review".to_string())),
button("Diagnostics").on_press(Message::RunCommand("diagnostics".to_string())),
button("Design").on_press(Message::RunCommand("design-board".to_string())),
]
.spacing(10)
.align_y(Center),
)
.padding(10)
.style(app_style::toolbar);
let editor = text_editor(&self.editor)
.placeholder("Write Rust code...")
.on_action(Message::EditorAction)
.height(Fill)
.padding(12)
.size(15)
.font(Font::MONOSPACE)
.wrapping(text::Wrapping::None)
.highlight(self.active_file.syntax(), highlighter::Theme::SolarizedDark);
column![toolbar, container(editor).style(app_style::editor_surface)]
.spacing(8)
.padding(10)
.into()
}
fn view_assistant(&self) -> Element<'_, Message> {
let document = self.assistant_document();
let mut rows = column![row![
text(self.assistant_mode.title()).size(21).width(Fill),
chip(format!("{} rows", document.rows.len())),
]
.spacing(8)
.align_y(Center),]
.spacing(6);
for row in document.visible_rows(0, 24) {
rows = rows.push(output_row(row));
}
if document.truncated {
rows = rows.push(text("Output was truncated by rae safety limits.").size(12));
}
container(scrollable(rows).height(Fill)).padding(10).into()
}
fn view_inspector(&self) -> Element<'_, Message> {
let source = self.editor.text();
let (line, column_index) = self.editor.cursor_position();
let current_line = self
.editor
.line(line)
.map(|line| line.to_string())
.unwrap_or_default();
let current = analyze_code_line(¤t_line);
let block_analysis = code_block_analysis(&source);
let diagnostics = collect_diagnostics(&source);
let query = WidgetCatalogQuery::new()
.text(self.command_palette.query.as_str())
.limit(5);
let matches = self.catalog.query(query);
let catalog_summary = self.catalog.summary();
let recipe = build_layout_recipe(
AppRecipeKind::DeveloperAssistant,
Rect::new(0.0, 0.0, 1280.0, 820.0),
);
let tokens = &self.catalog.token_presets;
let defaults = tokens
.first()
.map(|tokens| {
tokens.component_defaults(
WidgetVariant::Filled,
ComponentSize::Regular,
ComponentRole::Primary,
WidgetState::new().focused(true),
)
})
.map(|defaults| defaults.sanitized());
let mut body = column![
text("Inspector").size(22),
text(format!("Cursor: {}:{}", line + 1, column_index + 1)),
text(format!(
"Line: {} tokens, {} keywords, {} comments",
current.token_count, current.keyword_count, current.comment_count
)),
]
.spacing(8);
if self.show_metrics {
body = body.push(section(
"Code metrics",
column![
text(format!("Lines: {}", block_analysis.line_count)),
text(format!(
"Semantic lines: {}",
block_analysis.semantic_line_count
)),
text(format!("Comments: {}", block_analysis.comment_count)),
text(format!("Max width: {}", block_analysis.max_display_width)),
text(format!("Recipe slots: {}", recipe.slots.len())),
]
.spacing(4)
.into(),
));
}
let mut design_system = column![
text(format!("{} total widgets", catalog_summary.widget_count)),
text(format!("{} examples", catalog_summary.example_count)),
text(format!(
"{} layout recipes",
catalog_summary.recipe_kind_count
)),
]
.spacing(8);
for count in catalog_summary
.category_counts
.iter()
.filter(|count| count.count > 0)
.take(8)
{
design_system = design_system.push(
row![
chip(count.category.label()),
text(format!("{} widgets", count.count)).size(13),
]
.spacing(8)
.align_y(Center),
);
}
design_system = design_system.push(summary_counts_row(&catalog_summary));
body = body.push(section("Design system coverage", design_system.into()));
body = body.push(section("Effects", effects_showcase()));
let mut recipe_preview = column![text(format!(
"{}: {} slots",
recipe.kind.label(),
recipe.slots.len()
))]
.spacing(8);
for slot in recipe.slots.iter().take(6) {
recipe_preview = recipe_preview.push(recipe_slot_card(slot));
}
body = body.push(section("Recipe preview", recipe_preview.into()));
let mut diagnostic_list =
column![text(format!("{} findings", diagnostics.len()))].spacing(4);
if diagnostics.is_empty() {
diagnostic_list =
diagnostic_list.push(text("No diagnostics for the current buffer.").size(13));
}
for diagnostic in diagnostics.iter().take(5) {
diagnostic_list = diagnostic_list.push(
text(format!(
"L{} [{}] {}",
diagnostic.line + 1,
diagnostic.severity.label(),
diagnostic.message
))
.size(13),
);
}
body = body.push(section("Diagnostics", diagnostic_list.into()));
let mut catalog = column![
text(format!("{} widgets", self.catalog.widgets.len())),
text("Matched catalog surfaces").size(12),
]
.spacing(8);
for widget in matches {
catalog = catalog.push(catalog_card(widget));
}
body = body.push(section("Catalog", catalog.into()));
let mut token_list = column![text(format!("{} token presets", tokens.len()))].spacing(8);
for token in tokens.iter().take(4) {
token_list = token_list.push(token_card(token));
}
token_list = token_list.push(contrast_audit_card(tokens.first()));
if let Some(defaults) = defaults {
token_list = token_list.push(text(format!(
"Primary control: {:.0}px high, {:.0}px radius",
defaults.height_px, defaults.radius_px
)));
}
body = body.push(section("Tokens", token_list.into()));
container(scrollable(body).height(Fill)).padding(10).into()
}
fn assistant_document(&self) -> rae::OutputDocument {
let source = self.editor.text();
let (line, column_index) = self.editor.cursor_position();
let current_line = self
.editor
.line(line)
.map(|line| line.to_string())
.unwrap_or_default();
let current = analyze_code_line(¤t_line);
let block = code_block_analysis(&source);
let diagnostics = collect_diagnostics(&source);
let catalog_summary = self.catalog.summary();
let sample = source.lines().take(14).collect::<Vec<_>>().join("\n");
let markdown = match self.assistant_mode {
AssistantMode::Review => format!(
"# Review\n- Active file: {}\n- Cursor: {}:{}\n- Lines: {}\n- Semantic lines: {}\n- Current line tokens: {}\n\n```rust\n{}\n```",
self.active_file.title(),
line + 1,
column_index + 1,
block.line_count,
block.semantic_line_count,
current.token_count,
sample
),
AssistantMode::Formatted => format!(
"# Format pass\n- Normalized trailing whitespace.\n- Preserved {} lines.\n- Copy-safe output is produced by `rae::materialize_output`.\n\n```rust\n{}\n```",
block.line_count, sample
),
AssistantMode::Catalog => format!(
"# Design catalog\n- Widgets: {}\n- Token presets: {}\n- Layout recipes: {}\n- Query: `{}`\n\nUse the left command search to drive catalog matches in the inspector.",
self.catalog.widgets.len(),
self.catalog.token_presets.len(),
self.catalog.recipe_kinds.len(),
self.command_palette.query
),
AssistantMode::Diagnostics => format!(
"# Diagnostics\n- Active file: {}\n- Findings: {}\n\n{}",
self.active_file.title(),
diagnostics.len(),
diagnostics_markdown(&diagnostics)
),
AssistantMode::DesignBoard => format!(
"# Design board\n- Widgets: {}\n- Examples: {}\n- Token presets: {}\n- Layout recipes: {}\n\n{}\n\n{}",
catalog_summary.widget_count,
catalog_summary.example_count,
catalog_summary.token_preset_count,
catalog_summary.recipe_kind_count,
category_summary_markdown(&catalog_summary),
use_case_summary_markdown(&catalog_summary)
),
};
materialize_output(&markdown, OutputOptions::for_width(76).with_max_rows(32))
}
fn theme(&self) -> Theme {
Theme::Dark
}
fn subscription(&self) -> Subscription<Message> {
keyboard::on_key_press(|key, modifiers| {
if modifiers.command() {
handle_hotkey(key)
} else {
None
}
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EditorPane {
Files,
Editor,
Assistant,
Inspector,
}
impl EditorPane {
fn title(self) -> &'static str {
match self {
Self::Files => "Files & Commands",
Self::Editor => "Editor",
Self::Assistant => "Assistant Output",
Self::Inspector => "Inspector",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SampleFile {
Main,
Theme,
Shader,
}
impl SampleFile {
const ALL: [Self; 3] = [Self::Main, Self::Theme, Self::Shader];
fn title(self) -> &'static str {
match self {
Self::Main => "src/main.rs",
Self::Theme => "src/theme.rs",
Self::Shader => "shaders/panel.frag",
}
}
fn description(self) -> &'static str {
match self {
Self::Main => "Iced app entry and update loop",
Self::Theme => "Rae token defaults and component metrics",
Self::Shader => "Fragment shader panel treatment",
}
}
fn kind(self) -> &'static str {
match self {
Self::Main | Self::Theme => "rust",
Self::Shader => "glsl",
}
}
fn syntax(self) -> &'static str {
match self {
Self::Shader => "glsl",
Self::Main | Self::Theme => "rs",
}
}
fn source(self) -> &'static str {
match self {
Self::Main => MAIN_SOURCE,
Self::Theme => THEME_SOURCE,
Self::Shader => SHADER_SOURCE,
}
}
}
#[derive(Debug, Clone, Copy)]
enum AssistantMode {
Review,
Formatted,
Catalog,
Diagnostics,
DesignBoard,
}
impl AssistantMode {
fn title(self) -> &'static str {
match self {
Self::Review => "Assistant Review",
Self::Formatted => "Formatter Result",
Self::Catalog => "Design Catalog",
Self::Diagnostics => "Diagnostics",
Self::DesignBoard => "Design Board",
}
}
}
#[derive(Debug, Clone, Copy)]
enum DiagnosticSeverity {
Info,
Warning,
}
impl DiagnosticSeverity {
fn label(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
}
}
}
#[derive(Debug, Clone)]
struct EditorDiagnostic {
line: usize,
severity: DiagnosticSeverity,
message: String,
}
impl EditorDiagnostic {
fn new(line: usize, severity: DiagnosticSeverity, message: impl Into<String>) -> Self {
Self {
line,
severity,
message: message.into(),
}
}
}
fn view_pane_controls<'a>(
pane: pane_grid::Pane,
total_panes: usize,
is_maximized: bool,
) -> Element<'a, Message> {
let mut controls = row![].spacing(6);
if total_panes > 1 {
let (label, message) = if is_maximized {
("Restore", Message::RestorePanes)
} else {
("Maximize", Message::MaximizePane(pane))
};
controls = controls.push(
button(label)
.style(button::secondary)
.padding(3)
.on_press(message),
);
}
controls.into()
}
fn handle_hotkey(key: keyboard::Key) -> Option<Message> {
use iced::keyboard::key::Key;
let command = match key.as_ref() {
Key::Character("f") => "format",
Key::Character("r") => "review",
Key::Character("d") => "catalog",
Key::Character("g") => "diagnostics",
Key::Character("b") => "design-board",
Key::Character("m") => "metrics",
Key::Character("i") => "insert-component",
Key::Character("l") => "clear",
_ => return None,
};
Some(Message::RunCommand(command.to_string()))
}
fn chip<'a>(label: impl Into<String>) -> Element<'a, Message> {
container(text(label.into()).size(11))
.padding([4, 8])
.style(app_style::chip)
.into()
}
fn metric_tile<'a>(label: impl Into<String>, value: impl Into<String>) -> Element<'a, Message> {
container(
column![text(label.into()).size(11), text(value.into()).size(18),]
.spacing(2)
.align_x(Center),
)
.width(Length::Fixed(96.0))
.padding(10)
.style(app_style::metric_tile)
.into()
}
fn catalog_card<'a>(widget: &'a WidgetDescriptor) -> Element<'a, Message> {
let mut variant_row = row![].spacing(6).align_y(Center);
for variant in widget.variants.iter().take(3) {
variant_row = variant_row.push(chip(variant_label(*variant)));
}
let examples = widget
.examples
.iter()
.take(2)
.map(|example| example.title.as_str())
.collect::<Vec<_>>()
.join(" / ");
container(
column![
row![
column![text(&widget.name).size(16), text(&widget.id).size(11),]
.spacing(1)
.width(Fill),
chip(widget.category.label()),
]
.spacing(8)
.align_y(Center),
text(&widget.summary).size(12),
row![
chip(widget.complexity.label()),
chip(format!("{} states", widget.states.state_count())),
chip(format!("{} examples", widget.examples.len())),
]
.spacing(6),
variant_row,
text(format!("Examples: {examples}")).size(11),
]
.spacing(8),
)
.padding(10)
.style(app_style::catalog_card)
.into()
}
fn token_card<'a>(tokens: &'a DesignTokenSet) -> Element<'a, Message> {
container(
column![
row![
column![text(&tokens.name).size(15), text(&tokens.id).size(11),]
.spacing(1)
.width(Fill),
chip(tokens.density.label()),
]
.spacing(8)
.align_y(Center),
row![
swatch("accent", tokens.colors.accent),
swatch("info", tokens.colors.info),
swatch("success", tokens.colors.success),
swatch("warning", tokens.colors.warning),
]
.spacing(6),
]
.spacing(8),
)
.padding(10)
.style(app_style::catalog_card)
.into()
}
fn contrast_audit_card<'a>(tokens: Option<&DesignTokenSet>) -> Element<'a, Message> {
let Some(tokens) = tokens else {
return container(text("No token preset available").size(12))
.padding(10)
.style(app_style::catalog_card)
.into();
};
let primary = tokens.colors.text.contrast_ratio(tokens.colors.background);
let muted = tokens
.colors
.text_muted
.contrast_ratio(tokens.colors.background);
let accent = tokens
.colors
.accent
.contrast_ratio(tokens.colors.background);
container(
column![
row![
text("Contrast audit").size(15).width(Fill),
chip(tokens.id.clone()),
]
.spacing(8)
.align_y(Center),
row![
contrast_metric("text", primary),
contrast_metric("muted", muted),
contrast_metric("accent", accent),
]
.spacing(6),
]
.spacing(8),
)
.padding(10)
.style(app_style::catalog_card)
.into()
}
fn contrast_metric<'a>(label: &'static str, ratio: f32) -> Element<'a, Message> {
let grade = if ratio >= 7.0 {
"AAA"
} else if ratio >= 4.5 {
"AA"
} else {
"fail"
};
container(
column![
text(label).size(10),
text(format!("{ratio:.1}")).size(16),
text(grade).size(10),
]
.spacing(1)
.align_x(Center),
)
.width(Length::Fixed(62.0))
.padding(8)
.style(app_style::metric_tile)
.into()
}
fn recipe_slot_card<'a>(slot: &rae::LayoutRecipeSlot) -> Element<'a, Message> {
let width = slot.rect.width.max(0.0);
let height = slot.rect.height.max(0.0);
container(
row![
column![
text(slot.label.clone()).size(13),
text(slot.id.clone()).size(10),
]
.spacing(1)
.width(Fill),
chip(format!("{:?}", slot.kind).to_ascii_lowercase()),
text(format!("{width:.0} x {height:.0}")).size(11),
]
.spacing(8)
.align_y(Center),
)
.padding(8)
.style(app_style::interactive_card)
.into()
}
fn summary_counts_row<'a>(summary: &WidgetCatalogSummary) -> Element<'a, Message> {
let developer = summary
.use_case_counts
.iter()
.find(|count| count.use_case == WidgetUseCase::DeveloperTools)
.map(|count| count.count)
.unwrap_or_default();
let workflow = summary
.complexity_counts
.iter()
.find(|count| count.complexity == rae::WidgetComplexity::Workflow)
.map(|count| count.count)
.unwrap_or_default();
let composite = summary
.complexity_counts
.iter()
.find(|count| count.complexity == rae::WidgetComplexity::Composite)
.map(|count| count.count)
.unwrap_or_default();
row![
metric_tile("developer", developer.to_string()),
metric_tile("workflow", workflow.to_string()),
metric_tile("composite", composite.to_string()),
]
.spacing(8)
.align_y(Center)
.into()
}
fn effects_showcase<'a>() -> Element<'a, Message> {
let loaders = [
WidgetLoaderModel::new("build-progress", WidgetLoaderKind::Bar)
.with_label("build progress")
.with_progress(0.68)
.with_track_count(24)
.with_accent(Rgba::rgb8(86, 196, 255)),
WidgetLoaderModel::new("scanner", WidgetLoaderKind::Scanner)
.with_label("scanner")
.with_progress(0.34)
.with_track_count(28)
.with_accent(Rgba::rgb8(154, 129, 255)),
WidgetLoaderModel::new("matrix", WidgetLoaderKind::Matrix)
.with_label("matrix rain")
.with_track_count(32)
.with_accent(Rgba::rgb8(90, 238, 176)),
WidgetLoaderModel::new("grid", WidgetLoaderKind::GridPulse)
.with_label("grid pulse")
.with_track_count(32)
.with_accent(Rgba::rgb8(255, 188, 88)),
];
let mut content = column![
text("Renderer-neutral glow and loader models").size(13),
glow_surface_card(
WidgetGlowSurface::new("focus-glow", "Focused glow surface")
.with_state(WidgetState::new().focused(true).selected(true))
.with_accent(Rgba::rgb8(140, 170, 255)),
),
]
.spacing(8);
for loader in loaders {
content = content.push(loader_card(loader));
}
content.into()
}
fn loader_card<'a>(loader: WidgetLoaderModel) -> Element<'a, Message> {
let segments = loader.segments(840);
let active = segments.iter().filter(|segment| segment.active).count();
let skin = loader.shader_skin();
let mut track = row![].spacing(2).align_y(Center);
for segment in segments.into_iter().take(32) {
track = track.push(loader_segment(segment));
}
container(
column![
row![
column![
text(loader.label.clone()).size(14),
text(loader.id).size(10),
]
.spacing(1)
.width(Fill),
chip(loader.kind.label()),
chip(format!("{} active", active)),
chip(format!("{} layers", skin.len())),
]
.spacing(8)
.align_y(Center),
track,
]
.spacing(8),
)
.padding(10)
.style(app_style::catalog_card)
.into()
}
fn loader_segment<'a>(segment: WidgetLoaderSegment) -> Element<'a, Message> {
let fill = segment.color;
let border_alpha = if segment.active { 120 } else { 30 };
container(text(""))
.width(Length::Fixed(7.0))
.height(Length::Fixed(18.0))
.style(move |_theme| container::Style {
background: Some(iced_color(fill).into()),
border: Border {
width: 1.0,
color: app_style::rgba8(232, 242, 255, border_alpha),
..Border::default()
},
..Default::default()
})
.into()
}
fn glow_surface_card<'a>(surface: WidgetGlowSurface) -> Element<'a, Message> {
let glow = surface.glow(1_200);
let effects = surface.effects(1_200);
let skin = surface.shader_skin();
let border = glow.color;
container(
column![
row![
column![text(surface.label).size(14), text(surface.id).size(10),]
.spacing(1)
.width(Fill),
chip(format!("intensity {:.2}", glow.intensity)),
chip(format!("{} layers", skin.len())),
]
.spacing(8)
.align_y(Center),
row![
metric_tile("radius", format!("{:.0}", glow.radius_px)),
metric_tile("spread", format!("{:.0}", glow.spread_px)),
metric_tile("focus", format!("{:.2}", effects.focus_ring_alpha)),
]
.spacing(6),
]
.spacing(8),
)
.padding(10)
.style(move |_theme| container::Style {
text_color: Some(app_style::rgba8(234, 242, 255, 255)),
background: Some(app_style::rgba8(20, 26, 56, 236).into()),
border: Border {
width: 1.0,
color: iced_color(border.with_alpha(0.82)),
..Border::default()
},
..Default::default()
})
.into()
}
fn category_summary_markdown(summary: &WidgetCatalogSummary) -> String {
let mut lines = vec!["## Category coverage".to_string()];
for count in summary
.category_counts
.iter()
.filter(|count| count.count > 0)
.take(10)
{
lines.push(format!("- {}: {}", count.category.label(), count.count));
}
lines.join("\n")
}
fn use_case_summary_markdown(summary: &WidgetCatalogSummary) -> String {
let mut lines = vec!["## Use-case coverage".to_string()];
for count in summary
.use_case_counts
.iter()
.filter(|count| count.count > 0)
.take(8)
{
lines.push(format!("- {}: {}", count.use_case.label(), count.count));
}
lines.push("## Complexity mix".to_string());
for count in summary
.complexity_counts
.iter()
.filter(|count| count.count > 0)
{
lines.push(format!("- {}: {}", count.complexity.label(), count.count));
}
lines.join("\n")
}
fn swatch<'a>(label: impl Into<String>, color: Rgba) -> Element<'a, Message> {
let label = label.into();
container(text(label).size(10))
.width(Length::Fixed(58.0))
.padding([6, 8])
.style(move |_theme| {
let color = color.sanitized();
container::Style {
text_color: Some(readable_swatch_text(color)),
background: Some(iced_color(color).into()),
border: Border {
width: 1.0,
color: app_style::rgba8(255, 255, 255, 42),
..Border::default()
},
..Default::default()
}
})
.into()
}
fn iced_color(color: Rgba) -> Color {
let color = color.sanitized();
Color {
r: color.r,
g: color.g,
b: color.b,
a: color.a,
}
}
fn readable_swatch_text(color: Rgba) -> Color {
if color.relative_luminance() > 0.44 {
app_style::rgba8(10, 14, 26, 255)
} else {
app_style::rgba8(238, 244, 255, 255)
}
}
fn variant_label(variant: WidgetVariant) -> &'static str {
match variant {
WidgetVariant::Filled => "filled",
WidgetVariant::Tonal => "tonal",
WidgetVariant::Outline => "outline",
WidgetVariant::Ghost => "ghost",
WidgetVariant::Glass => "glass",
}
}
fn section<'a>(title: &'a str, content: Element<'a, Message>) -> Element<'a, Message> {
container(column![text(title).size(16), content].spacing(6))
.padding(10)
.style(app_style::section)
.into()
}
fn output_row<'a>(row: &OutputRow) -> Element<'a, Message> {
let (label, size) = match row {
OutputRow::Blank => (String::new(), 12),
OutputRow::Paragraph { text, .. } => (text.clone(), 14),
OutputRow::Heading { level, text } => (format!("{} {}", "#".repeat(*level), text), 18),
OutputRow::Bullet { text, .. } => (format!("- {text}"), 14),
OutputRow::Quote { text, .. } => (format!("> {text}"), 14),
OutputRow::CodeFenceStart { language } => (format!("```{language}"), 13),
OutputRow::Code { gutter, text, .. } => {
let gutter = gutter.as_deref().unwrap_or(" |");
(format!("{gutter} {text}"), 13)
}
OutputRow::CodeFenceEnd => ("```".to_string(), 13),
};
text(label).font(Font::MONOSPACE).size(size).into()
}
fn code_block_analysis(source: &str) -> rae::CodeBlockAnalysis {
let markdown = format!("```rust\n{source}\n```");
materialize_output(&markdown, OutputOptions::for_width(120).with_max_rows(512))
.code_block_analyses()
.into_iter()
.next()
.unwrap_or_default()
}
fn collect_diagnostics(source: &str) -> Vec<EditorDiagnostic> {
let mut diagnostics = Vec::new();
let mut brace_balance = 0isize;
let mut line_count = 0usize;
for (line_index, line) in source.lines().enumerate() {
line_count = line_index + 1;
let analysis = analyze_code_line(line);
let trimmed = line.trim();
if analysis.display_width > 100 {
diagnostics.push(EditorDiagnostic::new(
line_index,
DiagnosticSeverity::Warning,
format!("line is {} columns wide", analysis.display_width),
));
}
if trimmed.contains("todo!") || trimmed.contains("TODO") {
diagnostics.push(EditorDiagnostic::new(
line_index,
DiagnosticSeverity::Info,
"todo marker needs follow-up",
));
}
if trimmed.contains(".unwrap()") || trimmed.contains("unwrap(") {
diagnostics.push(EditorDiagnostic::new(
line_index,
DiagnosticSeverity::Warning,
"fallible path uses unwrap",
));
}
if trimmed.contains("expect(") || trimmed.contains("panic!") {
diagnostics.push(EditorDiagnostic::new(
line_index,
DiagnosticSeverity::Warning,
"panic path should be surfaced in UI",
));
}
brace_balance += line.matches('{').count() as isize;
brace_balance -= line.matches('}').count() as isize;
}
if brace_balance > 0 {
diagnostics.push(EditorDiagnostic::new(
line_count.saturating_sub(1),
DiagnosticSeverity::Warning,
"buffer appears to have unclosed braces",
));
} else if brace_balance < 0 {
diagnostics.push(EditorDiagnostic::new(
0,
DiagnosticSeverity::Warning,
"buffer has more closing braces than opening braces",
));
}
diagnostics
}
fn diagnostics_markdown(diagnostics: &[EditorDiagnostic]) -> String {
if diagnostics.is_empty() {
return "No diagnostics for the current buffer.".to_string();
}
diagnostics
.iter()
.take(16)
.map(|diagnostic| {
format!(
"- L{} [{}] {}",
diagnostic.line + 1,
diagnostic.severity.label(),
diagnostic.message
)
})
.collect::<Vec<_>>()
.join("\n")
}
fn format_source(source: &str) -> String {
let mut output = String::new();
let mut previous_blank = false;
for line in source.lines() {
let trimmed = line.trim_end();
let blank = trimmed.is_empty();
if blank && previous_blank {
continue;
}
output.push_str(trimmed);
output.push('\n');
previous_blank = blank;
}
output
}
fn command_items() -> Vec<CommandItem> {
vec![
CommandItem::new("format", "Format Buffer")
.with_subtitle("Normalize whitespace in the editor")
.with_keywords(["edit", "rust", "clean"])
.with_shortcut(Shortcut::new("F").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("review", "Review Current Buffer")
.with_subtitle("Materialize a safe assistant review")
.with_keywords(["assistant", "output", "analysis"])
.with_shortcut(Shortcut::new("R").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("catalog", "Show Design Catalog")
.with_subtitle("Query rae widgets and token presets")
.with_keywords(["design", "widgets", "tokens"])
.with_shortcut(Shortcut::new("D").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("diagnostics", "Show Diagnostics")
.with_subtitle("Find simple code health issues")
.with_keywords(["problems", "warnings", "lint"])
.with_shortcut(Shortcut::new("G").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("design-board", "Show Design Board")
.with_subtitle("Summarize catalog coverage, tokens, recipes, and use cases")
.with_keywords(["catalog", "tokens", "recipes", "coverage"])
.with_shortcut(Shortcut::new("B").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("metrics", "Toggle Metrics")
.with_subtitle("Show or hide code metrics")
.with_keywords(["inspector", "lines", "tokens"])
.with_shortcut(Shortcut::new("M").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("insert-component", "Insert Component Snippet")
.with_subtitle("Paste a small iced component function")
.with_keywords(["snippet", "component", "iced"])
.with_shortcut(Shortcut::new("I").with_modifiers([rae::KeyModifier::Ctrl])),
CommandItem::new("clear", "Clear Searches")
.with_subtitle("Reset command and snippet filters")
.with_keywords(["reset", "search"])
.with_shortcut(Shortcut::new("L").with_modifiers([rae::KeyModifier::Ctrl])),
]
}
fn snippets() -> Vec<TextSnippet> {
vec![
TextSnippet::new(
"component",
"\nfn metric_chip(label: &str, value: impl ToString) -> iced::Element<'_, Message> {\n iced::widget::container(\n iced::widget::row![iced::widget::text(label), iced::widget::text(value.to_string())]\n .spacing(8),\n )\n .padding(8)\n .into()\n}\n",
)
.with_description("Iced metric chip")
.with_keywords(["iced", "widget", "metrics"]),
TextSnippet::new(
"command",
"\nMessage::RunCommand(id) => self.run_command(&id),\n",
)
.with_description("Command routing match arm")
.with_keywords(["update", "message"]),
TextSnippet::new(
"sanitize",
"\nlet document = rae::materialize_output(markdown, rae::OutputOptions::for_width(88));\n",
)
.with_description("Safe assistant output materialization")
.with_keywords(["rae", "output", "assistant"]),
]
}
mod app_style {
use super::*;
pub 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,
}
}
pub fn app_background(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(226, 236, 255, 255)),
background: Some(rgba8(4, 7, 18, 255).into()),
..Default::default()
}
}
pub fn hero(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(232, 241, 255, 255)),
background: Some(rgba8(13, 20, 42, 242).into()),
border: Border {
width: 1.0,
color: rgba8(108, 142, 255, 96),
..Border::default()
},
..Default::default()
}
}
pub fn toolbar(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(232, 241, 255, 255)),
background: Some(rgba8(14, 22, 45, 235).into()),
border: Border {
width: 1.0,
color: rgba8(103, 130, 216, 84),
..Border::default()
},
..Default::default()
}
}
pub fn editor_surface(_theme: &Theme) -> container::Style {
container::Style {
background: Some(rgba8(6, 10, 22, 245).into()),
border: Border {
width: 1.0,
color: rgba8(65, 85, 142, 110),
..Border::default()
},
..Default::default()
}
}
pub fn section(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(225, 235, 255, 255)),
background: Some(rgba8(11, 18, 37, 218).into()),
border: Border {
width: 1.0,
color: rgba8(93, 114, 178, 74),
..Border::default()
},
..Default::default()
}
}
pub fn interactive_card(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(223, 234, 255, 255)),
background: Some(rgba8(16, 25, 50, 224).into()),
border: Border {
width: 1.0,
color: rgba8(92, 120, 205, 76),
..Border::default()
},
..Default::default()
}
}
pub fn selected_card(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(242, 247, 255, 255)),
background: Some(rgba8(31, 50, 94, 235).into()),
border: Border {
width: 1.0,
color: rgba8(124, 186, 255, 162),
..Border::default()
},
..Default::default()
}
}
pub fn catalog_card(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(225, 235, 255, 255)),
background: Some(rgba8(15, 24, 48, 232).into()),
border: Border {
width: 1.0,
color: rgba8(92, 128, 228, 90),
..Border::default()
},
..Default::default()
}
}
pub fn metric_tile(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(233, 241, 255, 255)),
background: Some(rgba8(25, 38, 73, 232).into()),
border: Border {
width: 1.0,
color: rgba8(128, 172, 255, 112),
..Border::default()
},
..Default::default()
}
}
pub fn chip(_theme: &Theme) -> container::Style {
container::Style {
text_color: Some(rgba8(188, 210, 255, 255)),
background: Some(rgba8(28, 40, 75, 210).into()),
border: Border {
width: 1.0,
color: rgba8(126, 163, 246, 78),
..Border::default()
},
..Default::default()
}
}
pub fn title_bar_active(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
text_color: Some(rgba8(201, 217, 250, 255)),
background: Some(rgba8(11, 17, 34, 245).into()),
border: Border {
width: 1.0,
color: palette.background.strong.color,
..Border::default()
},
..Default::default()
}
}
pub fn title_bar_focused(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
text_color: Some(rgba8(246, 250, 255, 255)),
background: Some(rgba8(35, 67, 126, 248).into()),
border: Border {
width: 1.0,
color: palette.primary.strong.color,
..Border::default()
},
..Default::default()
}
}
pub fn pane_active(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
text_color: Some(rgba8(226, 236, 255, 255)),
background: Some(rgba8(8, 13, 28, 240).into()),
border: Border {
width: 1.0,
color: palette.background.strong.color,
..Border::default()
},
..Default::default()
}
}
pub fn pane_focused(theme: &Theme) -> container::Style {
let palette = theme.extended_palette();
container::Style {
text_color: Some(rgba8(238, 245, 255, 255)),
background: Some(rgba8(12, 20, 44, 248).into()),
border: Border {
width: 2.0,
color: palette.primary.strong.color,
..Border::default()
},
..Default::default()
}
}
}
const MAIN_SOURCE: &str = r#"use iced::widget::{button, column, text};
use iced::Element;
fn main() -> iced::Result {
iced::run("demo", update, view)
}
#[derive(Default)]
struct State {
count: i64,
}
#[derive(Debug, Clone)]
enum Message {
Increment,
Decrement,
}
fn update(state: &mut State, message: Message) {
match message {
Message::Increment => state.count += 1,
Message::Decrement => state.count -= 1,
}
}
fn view(state: &State) -> Element<'_, Message> {
column![
button("+").on_press(Message::Increment),
text(state.count).size(48),
button("-").on_press(Message::Decrement),
]
.padding(24)
.into()
}
"#;
const THEME_SOURCE: &str = r#"use rae::{built_in_token_presets, ComponentRole, ComponentSize, WidgetState, WidgetVariant};
pub fn primary_button_metrics() -> String {
let tokens = built_in_token_presets().remove(0);
let defaults = tokens.component_defaults(
WidgetVariant::Filled,
ComponentSize::Regular,
ComponentRole::Primary,
WidgetState::new().focused(true),
);
format!("height={} radius={}", defaults.height_px, defaults.radius_px)
}
"#;
const SHADER_SOURCE: &str = r#"precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
uniform float u_intensity;
void main() {
vec2 uv = gl_FragCoord.xy / max(u_resolution.xy, vec2(1.0));
float band = smoothstep(0.2, 0.8, uv.x + sin(u_time + uv.y * 8.0) * 0.08);
vec3 color = mix(vec3(0.02, 0.03, 0.08), vec3(0.38, 0.24, 0.82), band);
color += vec3(0.1, 0.55, 0.9) * u_intensity * smoothstep(0.8, 0.2, abs(uv.y - 0.5));
gl_FragColor = vec4(color, 1.0);
}
"#;