use iced::widget::pane_grid;
use iced::widget::{
button, canvas, checkbox, column, combo_box, container, horizontal_rule, horizontal_space,
image, keyed_column, lazy, markdown, mouse_area, pane_grid as pane_grid_widget, pick_list,
progress_bar, qr_code, radio, responsive, rich_text, row, scrollable, slider, span, svg, text,
text_editor, text_input, themer, toggler, tooltip, vertical_rule, vertical_slider, Stack,
};
use iced::{alignment, font, mouse, Border, Center, Color, Element, Fill, Font, Length, Point};
use iced::{Rectangle, Renderer, Size, Task, Theme};
use rae::{
built_in_widget_catalog, OverlayKind, Rect, Rgba, WidgetGalleryItem, WidgetGalleryModel,
WidgetGlowSurface, WidgetLoaderKind, WidgetLoaderModel, WidgetState,
};
pub fn main() -> iced::Result {
iced::application(
"rae studio gallery",
StudioGallery::update,
StudioGallery::view,
)
.theme(StudioGallery::theme)
.window_size(Size::new(1440.0, 920.0))
.run_with(|| (StudioGallery::new(), Task::none()))
}
struct StudioGallery {
gallery: WidgetGalleryModel,
search: String,
hovered_id: Option<String>,
selected_id: String,
checkbox_on: bool,
toggler_on: bool,
slider_value: f32,
vertical_value: f32,
text_value: String,
editor: text_editor::Content,
family: WidgetFamily,
combo_state: combo_box::State<WidgetFamily>,
combo_selected: Option<WidgetFamily>,
mode: Option<GalleryMode>,
markdown: Vec<markdown::Item>,
qr_data: qr_code::Data,
image_handle: image::Handle,
svg_handle: svg::Handle,
panes: pane_grid::State<MiniPane>,
focused_pane: Option<pane_grid::Pane>,
floating_preview: bool,
}
#[derive(Debug, Clone)]
enum Message {
SearchChanged(String),
QuickSearch(&'static str),
Hovered(String),
HoverCleared(String),
Select(String),
ToggleFloatingPreview,
CheckboxChanged(bool),
TogglerChanged(bool),
SliderChanged(f32),
VerticalChanged(f32),
TextChanged(String),
EditorAction(text_editor::Action),
FamilyPicked(WidgetFamily),
ComboSelected(WidgetFamily),
ModeSelected(GalleryMode),
MarkdownLink(String),
PaneClicked(pane_grid::Pane),
PaneDragged(pane_grid::DragEvent),
PaneResized(pane_grid::ResizeEvent),
}
impl StudioGallery {
fn new() -> Self {
let mut gallery = WidgetGalleryModel::new(gallery_items()).with_max_results(96);
let selected_id = gallery
.selected()
.map(|item| item.id.clone())
.unwrap_or_else(|| "button".to_string());
gallery.select(&selected_id);
let (mut panes, first) = pane_grid::State::new(MiniPane::Canvas);
let _ = panes.split(pane_grid::Axis::Vertical, first, MiniPane::Inspector);
Self {
gallery,
search: String::new(),
hovered_id: None,
selected_id,
checkbox_on: true,
toggler_on: true,
slider_value: 68.0,
vertical_value: 42.0,
text_value: "hover glow".to_string(),
editor: text_editor::Content::with_text(
"fn render(surface: &rae::WidgetGalleryItem) {\n // renderer owns pixels; rae owns reusable model data\n}\n",
),
family: WidgetFamily::Inputs,
combo_state: combo_box::State::new(WidgetFamily::ALL.to_vec()),
combo_selected: Some(WidgetFamily::Effects),
mode: Some(GalleryMode::Production),
markdown: markdown::parse(MARKDOWN_SAMPLE).collect(),
qr_data: qr_code::Data::new("rae studio gallery").expect("static QR payload is valid"),
image_handle: generated_image(),
svg_handle: svg::Handle::from_memory(STUDIO_MARK.as_bytes().to_vec()),
panes,
focused_pane: Some(first),
floating_preview: true,
}
}
fn update(&mut self, message: Message) {
match message {
Message::SearchChanged(query) => {
self.search = query;
self.gallery.set_query(&self.search);
if let Some(item) = self.gallery.selected() {
self.selected_id = item.id.clone();
}
}
Message::QuickSearch(query) => {
self.search = query.to_string();
self.gallery.set_query(&self.search);
if let Some(item) = self.gallery.selected() {
self.selected_id = item.id.clone();
}
}
Message::Hovered(id) => {
self.hovered_id = Some(id.clone());
self.gallery.set_hovered_id(&id);
}
Message::HoverCleared(id) => {
if self.hovered_id.as_deref() == Some(id.as_str()) {
self.hovered_id = None;
self.gallery.clear_hover();
}
}
Message::Select(id) => {
if self.gallery.select(&id) {
self.selected_id = id;
}
}
Message::ToggleFloatingPreview => {
self.floating_preview = !self.floating_preview;
}
Message::CheckboxChanged(value) => self.checkbox_on = value,
Message::TogglerChanged(value) => self.toggler_on = value,
Message::SliderChanged(value) => self.slider_value = value,
Message::VerticalChanged(value) => self.vertical_value = value,
Message::TextChanged(value) => self.text_value = value,
Message::EditorAction(action) => self.editor.perform(action),
Message::FamilyPicked(family) => self.family = family,
Message::ComboSelected(family) => self.combo_selected = Some(family),
Message::ModeSelected(mode) => self.mode = Some(mode),
Message::MarkdownLink(url) => self.text_value = url,
Message::PaneClicked(pane) => self.focused_pane = 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);
}
}
}
fn view(&self) -> Element<'_, Message> {
let content = row![self.sidebar(), self.gallery_surface(), self.inspector()]
.spacing(14)
.height(Fill);
let mut stacked = Stack::new().push(content);
if self.floating_preview {
if let Some(item) = self.active_item() {
stacked = stacked.push(
container(self.preview_overlay(item))
.width(Fill)
.height(Fill)
.padding(28)
.align_x(alignment::Horizontal::Right)
.align_y(alignment::Vertical::Top),
);
}
}
container(stacked.width(Fill).height(Fill))
.padding(18)
.style(studio_style::app_background)
.into()
}
fn sidebar(&self) -> Element<'_, Message> {
let mut results = column![].spacing(8);
for item in self.gallery.filtered_items_limited(28) {
let id = item.id.clone();
let selected = self.selected_id == item.id;
let hovered = self.hovered_id.as_deref() == Some(item.id.as_str());
results = results.push(
mouse_area(
container(
row![
column![text(&item.title).size(14), text(&item.group).size(11)]
.spacing(1)
.width(Fill),
if item.featured {
chip("hero")
} else {
chip("base")
},
]
.spacing(8)
.align_y(Center),
)
.padding(10)
.width(Fill)
.style(move |_| studio_style::list_row(selected, hovered, item.accent)),
)
.on_enter(Message::Hovered(id.clone()))
.on_exit(Message::HoverCleared(id.clone()))
.on_press(Message::Select(id))
.interaction(mouse::Interaction::Pointer),
);
}
let quick = row![
filter_button("inputs"),
filter_button("overlay"),
filter_button("effects"),
]
.spacing(6);
container(
column![
text("Rae Studio").size(28),
text("iced widget index + rae interaction models").size(12),
text_input("Search widgets, effects, overlays", &self.search)
.on_input(Message::SearchChanged)
.padding(10),
quick,
horizontal_rule(1),
scrollable(results).height(Fill),
]
.spacing(12),
)
.width(Length::Fixed(292.0))
.height(Fill)
.padding(14)
.style(studio_style::sidebar)
.into()
}
fn gallery_surface(&self) -> Element<'_, Message> {
let summary = built_in_widget_catalog().summary();
let hero_loader = WidgetLoaderModel::new("studio.scan", WidgetLoaderKind::GridPulse)
.with_progress(self.slider_value / 100.0)
.with_track_count(32)
.with_accent(Rgba::rgb8(126, 178, 255));
let hero = container(
column![
row![
column![
text("Professional interaction surface").size(30),
text("Search, hover, selection, floating previews, glow samples, and widget cards share one renderer-neutral model.").size(13),
]
.spacing(4)
.width(Fill),
metric("catalog", summary.widget_count.to_string()),
metric("examples", summary.example_count.to_string()),
button(if self.floating_preview { "Hide preview" } else { "Show preview" })
.on_press(Message::ToggleFloatingPreview),
]
.spacing(12)
.align_y(Center),
loader_track(hero_loader),
]
.spacing(14),
)
.padding(18)
.style(studio_style::hero);
let mut grid = column![hero, section_title("Iced widget families")].spacing(14);
let matches = self.gallery.filtered_items_limited(90);
for chunk in matches.chunks(3) {
let mut line = row![].spacing(12).align_y(Center);
for item in chunk {
line = line.push(self.widget_card(item));
}
grid = grid.push(line);
}
container(scrollable(grid).height(Fill))
.height(Fill)
.width(Fill)
.into()
}
fn widget_card<'a>(&'a self, item: &'a WidgetGalleryItem) -> Element<'a, Message> {
let selected = self.selected_id == item.id;
let hovered = self.hovered_id.as_deref() == Some(item.id.as_str());
let id = item.id.clone();
let preview = self.widget_demo(item.id.as_str());
mouse_area(
container(
column![
row![
column![text(&item.title).size(18), text(&item.description).size(11)]
.spacing(2)
.width(Fill),
chip(&item.group),
]
.spacing(8)
.align_y(Center),
preview,
tag_row(&item.tags),
]
.spacing(10),
)
.width(Fill)
.height(Length::Fixed(if item.featured { 250.0 } else { 220.0 }))
.padding(14)
.style(move |_| studio_style::gallery_card(selected, hovered, item.accent)),
)
.on_enter(Message::Hovered(id.clone()))
.on_exit(Message::HoverCleared(id.clone()))
.on_press(Message::Select(id))
.interaction(mouse::Interaction::Pointer)
.into()
}
fn widget_demo<'a>(&'a self, id: &'a str) -> Element<'a, Message> {
match id {
"button" => row![
button("Primary").on_press(Message::QuickSearch("primary")),
button("Ghost")
.style(button::text)
.on_press(Message::QuickSearch("ghost")),
]
.spacing(8)
.into(),
"checkbox" => checkbox("Enable production checks", self.checkbox_on)
.on_toggle(Message::CheckboxChanged)
.into(),
"toggler" => toggler(self.toggler_on)
.label("Live glow overlay")
.on_toggle(Message::TogglerChanged)
.into(),
"radio" => column![
radio(
"Production",
GalleryMode::Production,
self.mode,
Message::ModeSelected,
),
radio(
"Audit",
GalleryMode::Audit,
self.mode,
Message::ModeSelected,
),
radio(
"Motion",
GalleryMode::Motion,
self.mode,
Message::ModeSelected,
),
]
.spacing(4)
.into(),
"slider" => column![
text(format!("Glow intensity {:.0}%", self.slider_value)).size(12),
slider(0.0..=100.0, self.slider_value, Message::SliderChanged),
progress_bar(0.0..=100.0, self.slider_value),
]
.spacing(8)
.into(),
"vertical_slider" => row![
vertical_slider(0.0..=100.0, self.vertical_value, Message::VerticalChanged)
.height(Length::Fixed(120.0)),
column![
text("Vertical level").size(13),
text(format!("{:.0}", self.vertical_value)).size(28)
]
.spacing(2),
]
.spacing(16)
.align_y(Center)
.into(),
"text_input" => text_input("Type a query", &self.text_value)
.on_input(Message::TextChanged)
.padding(10)
.into(),
"text_editor" => text_editor(&self.editor)
.on_action(Message::EditorAction)
.height(Length::Fixed(112.0))
.padding(10)
.font(Font::MONOSPACE)
.into(),
"combo_box" => combo_box(
&self.combo_state,
"Search widget family",
self.combo_selected.as_ref(),
Message::ComboSelected,
)
.padding(10)
.into(),
"pick_list" => pick_list(WidgetFamily::ALL, Some(self.family), Message::FamilyPicked)
.placeholder("Choose family")
.padding(10)
.into(),
"progress_bar" => column![
progress_bar(0.0..=100.0, self.slider_value),
loader_track(
WidgetLoaderModel::new("loader.rings", WidgetLoaderKind::Rings)
.with_track_count(24)
.with_accent(Rgba::rgb8(120, 230, 190)),
),
]
.spacing(10)
.into(),
"tooltip" => tooltip(
button("Hover detail").style(button::secondary),
container(text("Tooltip content styled as a preview card").size(12))
.padding(10)
.style(studio_style::tooltip_card),
tooltip::Position::Top,
)
.gap(8)
.into(),
"mouse_area" => mouse_area(
container(text("Hover target updates the gallery overlay").size(13))
.padding(16)
.width(Fill)
.style(studio_style::hotspot),
)
.on_enter(Message::Hovered("mouse_area".to_string()))
.on_exit(Message::HoverCleared("mouse_area".to_string()))
.interaction(mouse::Interaction::Pointer)
.into(),
"container" => container(text("Layered glass panel").size(17))
.padding(20)
.center_x(Fill)
.style(studio_style::demo_container)
.into(),
"row_column" => column![
row![pill("Row"), pill("align"), pill("spacing")].spacing(8),
row![pill("Column"), pill("stacked"), pill("content")].spacing(8),
]
.spacing(8)
.into(),
"scrollable" => scrollable(
column![
text("Scrollable activity log").size(13),
text("Search updated"),
text("Hover overlay armed"),
text("Glow surface resolved"),
text("Renderer draws final pixels"),
]
.spacing(8),
)
.height(Length::Fixed(112.0))
.into(),
"rule" => row![
column![text("Horizontal"), horizontal_rule(2), text("Rule")]
.spacing(8)
.width(Fill),
vertical_rule(2),
column![text("Vertical"), text("Rule")].spacing(8),
]
.spacing(12)
.height(Length::Fixed(96.0))
.into(),
"space" => row![text("Left"), horizontal_space(), text("Right")]
.height(Length::Fixed(90.0))
.align_y(Center)
.into(),
"stack" => Stack::new()
.push(
container(text("Base card"))
.padding(26)
.style(studio_style::demo_container),
)
.push(
container(text("overlay").size(11))
.padding([4, 8])
.style(studio_style::floating_badge),
)
.height(Length::Fixed(110.0))
.into(),
"pane_grid" => pane_grid_widget(&self.panes, |pane, state, _| {
let label = match state {
MiniPane::Canvas => "Canvas",
MiniPane::Inspector => "Inspector",
};
let title = pane_grid::TitleBar::new(text(label).size(12))
.padding(4)
.style(if self.focused_pane == Some(pane) {
studio_style::pane_title_focused
} else {
studio_style::pane_title
});
pane_grid::Content::new(container(text(label).size(12)).padding(10))
.title_bar(title)
.style(studio_style::pane_body)
})
.height(Length::Fixed(128.0))
.spacing(6)
.on_click(Message::PaneClicked)
.on_drag(Message::PaneDragged)
.on_resize(6, Message::PaneResized)
.into(),
"keyed_column" => keyed_column([
(1_u8, text("Stable row A").size(13).into()),
(2_u8, text("Stable row B").size(13).into()),
(3_u8, text("Stable row C").size(13).into()),
])
.spacing(6)
.into(),
"text" => text("Display text with scale, color, wrapping, and font choices")
.size(18)
.width(Fill)
.into(),
"rich_text" => rich_text![
span("Design ").color(studio_style::rgba8(220, 232, 255, 255)),
span("studio ")
.color(studio_style::rgba8(120, 200, 255, 255))
.font(Font {
weight: font::Weight::Bold,
..Font::default()
}),
span("states").color(studio_style::rgba8(255, 188, 96, 255)),
]
.size(20)
.into(),
"themer" => themer(
Theme::Light,
container(text("Nested light theme surface").size(13))
.padding(14)
.style(container::rounded_box),
)
.into(),
"lazy" => lazy(
self.slider_value.round() as u32,
|value| -> Element<'static, Message> {
container(text(format!("Lazy cache key {value}")))
.padding(14)
.style(studio_style::demo_container)
.into()
},
)
.into(),
"responsive" => container(responsive(|size| {
container(
text(format!("Responsive {:.0} x {:.0}", size.width, size.height)).size(13),
)
.padding(14)
.style(studio_style::demo_container)
.into()
}))
.height(Length::Fixed(104.0))
.into(),
"canvas" => canvas(StudioCanvas)
.width(Fill)
.height(Length::Fixed(120.0))
.into(),
"image" => image(self.image_handle.clone())
.width(Fill)
.height(Length::Fixed(118.0))
.into(),
"svg" => svg(self.svg_handle.clone())
.width(Fill)
.height(Length::Fixed(118.0))
.into(),
"qr_code" => qr_code(&self.qr_data).cell_size(3).into(),
"markdown" => markdown::view(
&self.markdown,
markdown::Settings::default(),
markdown::Style::from_palette(Theme::TokyoNight.palette()),
)
.map(|url| Message::MarkdownLink(url.to_string())),
"shader" => container(
column![
text("Shader widget").size(16),
text("Requires a renderer-owned wgpu primitive pipeline.").size(12),
loader_track(
WidgetLoaderModel::new("shader.energy", WidgetLoaderKind::LaserSweep)
.with_accent(Rgba::rgb8(180, 120, 255)),
),
]
.spacing(8),
)
.padding(12)
.style(studio_style::demo_container)
.into(),
_ => text("Widget surface").into(),
}
}
fn inspector(&self) -> Element<'_, Message> {
let item = self.active_item();
let glow = item
.map(|item| item.hover_glow_surface())
.unwrap_or_else(|| WidgetGlowSurface::new("none", "No selection"));
let glow_sample = glow.glow(1_400);
let overlay = item.map(|item| item.preview_overlay(OverlayKind::Inspector));
let tags = item
.map(|item| item.tags.join(" / "))
.unwrap_or_else(|| "none".to_string());
container(
column![
text("Inspector").size(22),
text(item.map(|item| item.title.as_str()).unwrap_or("No widget")).size(16),
text(item.map(|item| item.description.as_str()).unwrap_or("Hover or select a widget card.")).size(12),
horizontal_rule(1),
metric("glow", format!("{:.2}", glow_sample.intensity)),
metric("radius", format!("{:.0}px", glow_sample.radius_px)),
metric("spread", format!("{:.0}px", glow_sample.spread_px)),
text(format!("Tags: {tags}")).size(12),
text(format!(
"Overlay: {}",
overlay
.as_ref()
.map(|overlay| overlay.title.as_str())
.unwrap_or("inactive")
))
.size(12),
section_title("Rae model output"),
text("WidgetGalleryModel filters search text, tracks hover/selection, and produces glow/overlay models for renderers.").size(12),
loader_track(
WidgetLoaderModel::new("inspector.scan", WidgetLoaderKind::Scanner)
.with_track_count(26)
.with_accent(Rgba::rgb8(255, 188, 96)),
),
]
.spacing(12),
)
.width(Length::Fixed(320.0))
.height(Fill)
.padding(14)
.style(studio_style::inspector)
.into()
}
fn preview_overlay<'a>(&'a self, item: &'a WidgetGalleryItem) -> Element<'a, Message> {
let surface = item.hover_glow_surface();
let glow = surface.glow(2_000);
container(
column![
row![
column![text(&item.title).size(20), text(&item.group).size(11)]
.spacing(2)
.width(Fill),
chip("hover overlay"),
]
.spacing(8)
.align_y(Center),
text(&item.description).size(12),
row![
metric("intensity", format!("{:.2}", glow.intensity)),
metric("radius", format!("{:.0}", glow.radius_px)),
metric("tags", item.tags.len().to_string()),
]
.spacing(8),
]
.spacing(10),
)
.width(Length::Fixed(360.0))
.padding(16)
.style(move |_| studio_style::floating_overlay(item.accent))
.into()
}
fn active_item(&self) -> Option<&WidgetGalleryItem> {
self.gallery.hovered().or_else(|| self.gallery.selected())
}
fn theme(&self) -> Theme {
Theme::TokyoNightStorm
}
}
fn filter_button(query: &'static str) -> Element<'static, Message> {
button(text(query).size(11))
.style(button::secondary)
.padding([5, 8])
.on_press(Message::QuickSearch(query))
.into()
}
fn section_title<'a>(title: &'a str) -> Element<'a, Message> {
text(title).size(16).into()
}
fn chip<'a>(label: impl Into<String>) -> Element<'a, Message> {
container(text(label.into()).size(10))
.padding([4, 8])
.style(studio_style::chip)
.into()
}
fn pill<'a>(label: impl Into<String>) -> Element<'a, Message> {
container(text(label.into()).size(12))
.padding([8, 12])
.style(studio_style::pill)
.into()
}
fn metric<'a>(label: impl Into<String>, value: impl Into<String>) -> Element<'a, Message> {
container(
column![text(label.into()).size(10), text(value.into()).size(17)]
.spacing(1)
.align_x(Center),
)
.width(Length::Fixed(84.0))
.padding(8)
.style(studio_style::metric)
.into()
}
fn tag_row<'a>(tags: &'a [String]) -> Element<'a, Message> {
let mut row = row![].spacing(6).align_y(Center);
for tag in tags.iter().take(4) {
row = row.push(chip(tag));
}
row.into()
}
fn loader_track<'a>(loader: WidgetLoaderModel) -> Element<'a, Message> {
let mut track = row![].spacing(2).align_y(Center);
for segment in loader.segments(1_100).into_iter().take(32) {
let color = segment.color;
let border = if segment.active { 120 } else { 28 };
track = track.push(
container(text(""))
.width(Length::Fixed(8.0))
.height(Length::Fixed(16.0))
.style(move |_| studio_style::loader_segment(color, border)),
);
}
track.into()
}
fn gallery_items() -> Vec<WidgetGalleryItem> {
let specs = [
(
"button",
"Button",
"actions",
"Command surfaces with hover and pressed states",
["action", "click", "primary"],
),
(
"checkbox",
"Checkbox",
"selection",
"Binary choice with check state",
["selection", "form", "input"],
),
(
"toggler",
"Toggler",
"selection",
"Switch control for binary settings",
["switch", "settings", "input"],
),
(
"radio",
"Radio",
"selection",
"Exclusive choice group",
["choice", "form", "selection"],
),
(
"slider",
"Slider",
"inputs",
"Horizontal scalar control",
["range", "audio", "input"],
),
(
"vertical_slider",
"Vertical Slider",
"inputs",
"Vertical scalar control",
["range", "level", "input"],
),
(
"text_input",
"Text Input",
"inputs",
"Single-line editable search surface",
["search", "text", "input"],
),
(
"text_editor",
"Text Editor",
"text",
"Multiline editor with actions",
["code", "editor", "text"],
),
(
"combo_box",
"Combo Box",
"inputs",
"Searchable dropdown selection",
["search", "menu", "input"],
),
(
"pick_list",
"Pick List",
"inputs",
"Dropdown list selection",
["menu", "select", "input"],
),
(
"progress_bar",
"Progress Bar",
"feedback",
"Progress surface paired with rae loaders",
["loading", "status", "effects"],
),
(
"tooltip",
"Tooltip",
"overlay",
"Hover hint surface",
["hover", "overlay", "help"],
),
(
"mouse_area",
"Mouse Area",
"overlay",
"Pointer enter/exit/press capture",
["hover", "pointer", "overlay"],
),
(
"container",
"Container",
"layout",
"Styled single-child surface",
["layout", "surface", "glass"],
),
(
"row_column",
"Row + Column",
"layout",
"Core horizontal and vertical composition",
["layout", "spacing", "align"],
),
(
"scrollable",
"Scrollable",
"layout",
"Scrollable content viewport",
["scroll", "viewport", "layout"],
),
(
"rule",
"Rule",
"layout",
"Horizontal and vertical separators",
["divider", "layout", "rule"],
),
(
"space",
"Space",
"layout",
"Flexible empty layout space",
["fill", "layout", "space"],
),
(
"stack",
"Stack",
"layout",
"Layered surfaces and overlay composition",
["overlay", "layer", "layout"],
),
(
"pane_grid",
"Pane Grid",
"layout",
"Resizable split-pane workspace",
["panes", "resize", "desktop"],
),
(
"keyed_column",
"Keyed Column",
"layout",
"Stable keyed list layout",
["list", "keyed", "layout"],
),
(
"text",
"Text",
"text",
"Display text primitive",
["copy", "type", "text"],
),
(
"rich_text",
"Rich Text",
"text",
"Styled span composition",
["span", "style", "text"],
),
(
"themer",
"Themer",
"theme",
"Nested theme boundary",
["theme", "surface", "style"],
),
(
"lazy",
"Lazy",
"performance",
"Dependency-keyed cached subtree",
["cache", "lazy", "performance"],
),
(
"responsive",
"Responsive",
"layout",
"Size-aware content surface",
["layout", "adaptive", "responsive"],
),
(
"canvas",
"Canvas",
"media",
"Immediate 2D drawing surface",
["draw", "media", "effects"],
),
(
"image",
"Image",
"media",
"Generated in-memory raster surface",
["media", "raster", "asset"],
),
(
"svg",
"SVG",
"media",
"Generated vector mark",
["media", "vector", "asset"],
),
(
"qr_code",
"QR Code",
"media",
"Matrix code surface",
["media", "matrix", "code"],
),
(
"markdown",
"Markdown",
"text",
"Parsed rich document output",
["document", "text", "output"],
),
(
"shader",
"Shader",
"effects",
"wgpu custom shader surface placeholder",
["shader", "effects", "wgpu"],
),
];
specs
.into_iter()
.enumerate()
.map(|(index, (id, title, group, description, tags))| {
let x = (index % 3) as f32 * 220.0;
let y = (index / 3) as f32 * 180.0;
WidgetGalleryItem::new(id, title)
.with_group(group)
.with_description(description)
.with_tags(tags)
.with_rect(Rect::new(x, y, 200.0, 156.0))
.with_accent(accent_for(index))
.featured(matches!(
id,
"button" | "text_input" | "stack" | "canvas" | "shader"
))
.with_state(if index % 4 == 0 {
WidgetState::new().focused(true)
} else {
WidgetState::new()
})
})
.collect()
}
fn accent_for(index: usize) -> Rgba {
const COLORS: [Rgba; 6] = [
Rgba::rgb8(120, 185, 255),
Rgba::rgb8(154, 129, 255),
Rgba::rgb8(90, 226, 180),
Rgba::rgb8(255, 188, 96),
Rgba::rgb8(255, 112, 158),
Rgba::rgb8(146, 220, 255),
];
COLORS[index % COLORS.len()]
}
fn generated_image() -> image::Handle {
let width = 72;
let height = 72;
let mut pixels = Vec::with_capacity((width * height * 4) as usize);
for y in 0..height {
for x in 0..width {
let fx = x as f32 / (width - 1) as f32;
let fy = y as f32 / (height - 1) as f32;
pixels.push((20.0 + fx * 80.0) as u8);
pixels.push((80.0 + fy * 150.0) as u8);
pixels.push((180.0 + fx * 60.0) as u8);
pixels.push(255);
}
}
image::Handle::from_rgba(width, height, pixels)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WidgetFamily {
Actions,
Inputs,
Layout,
Overlays,
Effects,
Media,
}
impl WidgetFamily {
const ALL: [Self; 6] = [
Self::Actions,
Self::Inputs,
Self::Layout,
Self::Overlays,
Self::Effects,
Self::Media,
];
}
impl std::fmt::Display for WidgetFamily {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Actions => "Actions",
Self::Inputs => "Inputs",
Self::Layout => "Layout",
Self::Overlays => "Overlays",
Self::Effects => "Effects",
Self::Media => "Media",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GalleryMode {
Production,
Audit,
Motion,
}
impl std::fmt::Display for GalleryMode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Production => "Production",
Self::Audit => "Audit",
Self::Motion => "Motion",
})
}
}
#[derive(Debug, Clone, Copy)]
enum MiniPane {
Canvas,
Inspector,
}
#[derive(Debug, Clone, Copy)]
struct StudioCanvas;
impl<Message> canvas::Program<Message> for StudioCanvas {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Vec<canvas::Geometry> {
let mut frame = canvas::Frame::new(renderer, bounds.size());
let base = canvas::Path::rectangle(Point::ORIGIN, bounds.size());
frame.fill(&base, Color::from_rgba(0.05, 0.08, 0.17, 1.0));
for index in 0..6 {
let center = Point::new(24.0 + index as f32 * 36.0, 34.0 + (index % 2) as f32 * 34.0);
let circle = canvas::Path::circle(center, 18.0 + index as f32 * 2.0);
frame.fill(
&circle,
Color::from_rgba(0.20 + index as f32 * 0.06, 0.42, 0.90, 0.18),
);
}
let line = canvas::Path::line(
Point::new(16.0, 96.0),
Point::new(bounds.width - 16.0, 24.0),
);
frame.stroke(
&line,
canvas::Stroke::default()
.with_width(2.0)
.with_color(Color::from_rgba(0.65, 0.86, 1.0, 0.72)),
);
vec![frame.into_geometry()]
}
}
const MARKDOWN_SAMPLE: &str = r#"## Output
- Search filters the gallery.
- Hover resolves glow and overlay data.
- Iced owns rendering.
"#;
const STUDIO_MARK: &str = r##"<svg width="160" height="100" viewBox="0 0 160 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="160" height="100" rx="24" fill="#101832"/>
<circle cx="46" cy="50" r="24" fill="#7AB9FF" fill-opacity="0.76"/>
<circle cx="84" cy="50" r="24" fill="#9A81FF" fill-opacity="0.76"/>
<circle cx="122" cy="50" r="24" fill="#5AE2B4" fill-opacity="0.76"/>
<path d="M30 76C52 34 93 31 130 24" stroke="#F4FAFF" stroke-width="5" stroke-linecap="round"/>
</svg>"##;
mod studio_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(234, 242, 255, 255)),
background: Some(rgba8(5, 8, 20, 255).into()),
..Default::default()
}
}
pub fn sidebar(_theme: &Theme) -> container::Style {
panel(rgba8(12, 18, 38, 244), rgba8(91, 122, 205, 96))
}
pub fn inspector(_theme: &Theme) -> container::Style {
panel(rgba8(14, 20, 42, 242), rgba8(125, 166, 255, 88))
}
pub fn hero(_theme: &Theme) -> container::Style {
panel(rgba8(18, 27, 58, 244), rgba8(120, 185, 255, 138))
}
pub fn gallery_card(selected: bool, hovered: bool, accent: Rgba) -> container::Style {
let accent = iced_color(accent);
let background = if selected {
rgba8(25, 42, 84, 246)
} else if hovered {
rgba8(20, 34, 72, 242)
} else {
rgba8(13, 21, 44, 232)
};
let border_alpha = if selected || hovered { 210.0 } else { 92.0 } / 255.0;
container::Style {
text_color: Some(rgba8(232, 241, 255, 255)),
background: Some(background.into()),
border: Border {
width: if selected { 2.0 } else { 1.0 },
color: Color {
a: border_alpha,
..accent
},
..Border::default()
},
..Default::default()
}
}
pub fn list_row(selected: bool, hovered: bool, accent: Rgba) -> container::Style {
gallery_card(selected, hovered, accent)
}
pub fn floating_overlay(accent: Rgba) -> container::Style {
let accent = iced_color(accent);
container::Style {
text_color: Some(rgba8(244, 249, 255, 255)),
background: Some(rgba8(15, 22, 48, 246).into()),
border: Border {
width: 1.0,
color: Color { a: 0.82, ..accent },
..Border::default()
},
..Default::default()
}
}
pub fn chip(_theme: &Theme) -> container::Style {
panel(rgba8(27, 40, 78, 220), rgba8(130, 166, 248, 72))
}
pub fn pill(_theme: &Theme) -> container::Style {
panel(rgba8(24, 38, 80, 228), rgba8(120, 185, 255, 98))
}
pub fn metric(_theme: &Theme) -> container::Style {
panel(rgba8(25, 39, 78, 236), rgba8(132, 178, 255, 110))
}
pub fn demo_container(_theme: &Theme) -> container::Style {
panel(rgba8(18, 29, 61, 230), rgba8(97, 130, 210, 86))
}
pub fn hotspot(_theme: &Theme) -> container::Style {
panel(rgba8(25, 38, 82, 238), rgba8(255, 188, 96, 136))
}
pub fn tooltip_card(_theme: &Theme) -> container::Style {
panel(rgba8(28, 36, 72, 248), rgba8(122, 185, 255, 132))
}
pub fn floating_badge(_theme: &Theme) -> container::Style {
panel(rgba8(75, 50, 125, 238), rgba8(190, 160, 255, 160))
}
pub fn pane_title(_theme: &Theme) -> container::Style {
panel(rgba8(12, 18, 38, 242), rgba8(90, 118, 190, 82))
}
pub fn pane_title_focused(_theme: &Theme) -> container::Style {
panel(rgba8(30, 58, 110, 246), rgba8(130, 185, 255, 150))
}
pub fn pane_body(_theme: &Theme) -> container::Style {
panel(rgba8(9, 14, 30, 236), rgba8(80, 105, 168, 76))
}
pub fn loader_segment(color: Rgba, border_alpha: u8) -> container::Style {
container::Style {
background: Some(iced_color(color).into()),
border: Border {
width: 1.0,
color: rgba8(232, 242, 255, border_alpha),
..Border::default()
},
..Default::default()
}
}
fn panel(background: Color, border_color: Color) -> container::Style {
container::Style {
text_color: Some(rgba8(232, 241, 255, 255)),
background: Some(background.into()),
border: Border {
width: 1.0,
color: border_color,
..Border::default()
},
..Default::default()
}
}
fn iced_color(color: Rgba) -> Color {
let color = color.sanitized();
Color {
r: color.r,
g: color.g,
b: color.b,
a: color.a,
}
}
}