use std::collections::BTreeMap;
use std::time::Duration;
use gpui_rhai::gpui::prelude::*;
use gpui_rhai::{
AppManifest, AssetData, CapabilityDescriptor, CapabilityId, CapabilityMethod, ChartDataLimits,
ChartDataset, ChartGeoMap, EmbeddedScriptSource, EmbeddedScriptView, HostSlotRegistry,
ModuleId, NativeChartData, NativeCollection, NativeEvent, NativeHandlerDescriptor,
NativeHandlerId, NativeTextDocument, ObjectField, PreparedScriptView, RuntimeEngine,
ScriptViewExtension, SubscriptionCapabilityHandler, SubscriptionWork, ThemeMode,
ThemeTokenOverrides, ThemeVariant, UiRuntimeState, UiValue, ValueSchema, extract_imports,
load_theme_source,
};
use gpui_rhai_registry::{
AR_LOCALE, BUNDLED_ASSET_SOURCES, BUNDLED_CHART_SOURCES_BY_ID, BUNDLED_COMPONENT_SOURCES_BY_ID,
BUNDLED_MOTION_SOURCES_BY_ID, BUNDLED_STORIES, BUNDLED_THEME_SOURCES, EN_LOCALE,
HOST_RESIDENT_STORY_SOURCE, StoryDefinition, ZH_CN_LOCALE,
};
pub const DEFAULT_STORY: &str = "components/button";
const COMPONENT_CATALOG_COLOR_TOKENS: &[&str] = &[
"surface",
"surface_raised",
"surface_hover",
"text_primary",
"text_muted",
"accent",
"accent_hover",
"on_accent",
"danger",
"on_danger",
"warning",
"on_warning",
"success",
"on_success",
"border",
"focus_ring",
"selection",
"disabled",
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GalleryLaunch {
pub story: String,
pub case: String,
pub theme: String,
pub locale: String,
pub motion: gpui_rhai::MotionPreference,
}
impl Default for GalleryLaunch {
fn default() -> Self {
Self {
story: DEFAULT_STORY.to_owned(),
case: "basic".to_owned(),
theme: "default-dark".to_owned(),
locale: "en".to_owned(),
motion: gpui_rhai::MotionPreference::Normal,
}
}
}
pub fn parse_motion_preference(value: &str) -> Result<gpui_rhai::MotionPreference, String> {
match value {
"normal" => Ok(gpui_rhai::MotionPreference::Normal),
"reduced" => Ok(gpui_rhai::MotionPreference::Reduced),
"none" => Ok(gpui_rhai::MotionPreference::None),
_ => Err(format!(
"unknown Gallery motion preference `{value}`; expected normal, reduced, or none"
)),
}
}
#[must_use]
pub fn stories() -> &'static [StoryDefinition] {
BUNDLED_STORIES
}
#[must_use]
pub fn list_text() -> String {
BUNDLED_STORIES
.iter()
.map(|story| {
let cases = story
.cases
.iter()
.map(|case| case.id)
.collect::<Vec<_>>()
.join(",");
format!(
"{}\t{}\t{}\t{}",
story.id, story.category, cases, story.title
)
})
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn resolve_story(id: &str, case: &str) -> Result<&'static StoryDefinition, String> {
let story = BUNDLED_STORIES
.iter()
.find(|story| story.id == id)
.ok_or_else(|| format!("unknown Gallery story `{id}`; run `gpui-rhai gallery --list`"))?;
if !story.cases.iter().any(|candidate| candidate.id == case) {
return Err(format!(
"unknown case `{case}` for Gallery story `{id}`; available: {}",
story
.cases
.iter()
.map(|candidate| candidate.id)
.collect::<Vec<_>>()
.join(", ")
));
}
Ok(story)
}
fn operations_fixture_case(case: &str) -> &str {
match case {
"failure-terminal" => "failure",
"loading" | "empty" | "failure" | "streaming" | "large" => case,
_ => "normal",
}
}
fn materialize_story_source(
story: &StoryDefinition,
launch: &GalleryLaunch,
selected: &ThemeVariant,
) -> Result<String, String> {
if story.id == "apps/operations" {
let page = match launch.case.as_str() {
"config-diff" | "failure" => "configurations",
"failure-terminal" => "deployments",
"theme-overrides" => "settings",
"large" => "hosts",
_ => "dashboard",
};
let fixture_case = operations_fixture_case(&launch.case);
let source = story.source.replace(
"__OPERATIONS_PAGE__",
&serde_json::to_string(page).map_err(|error| error.to_string())?,
);
let source = source.replace(
"__OPERATIONS_CASE__",
&serde_json::to_string(fixture_case).map_err(|error| error.to_string())?,
);
let command_open = launch.case == "command-dialog";
let failure_terminal = launch.case == "failure-terminal";
Ok(source
.replace("__OPERATIONS_COMMAND_OPEN__", &command_open.to_string())
.replace(
"__OPERATIONS_DEPLOYMENT_PHASE__",
if failure_terminal {
"\"failed\""
} else {
"\"idle\""
},
)
.replace(
"__OPERATIONS_DEPLOYMENT_PROGRESS__",
if failure_terminal { "48.0" } else { "0.0" },
)
.replace(
"__OPERATIONS_TOAST_VISIBLE__",
&failure_terminal.to_string(),
))
} else if story.id == "components/catalog" {
let json = |value: &str| serde_json::to_string(value).map_err(|error| error.to_string());
let category = if launch.case == "basic" {
"foundations"
} else {
launch.case.as_str()
};
let mode = match selected.mode {
ThemeMode::Light => "light",
ThemeMode::Dark => "dark",
};
let mut source = story.source.to_owned();
for (placeholder, value) in [
("__PATH__", json("")?),
("__FAMILY__", json(&selected.family)?),
("__VARIANT__", json(&selected.name)?),
("__MODE__", json(mode)?),
("__STATUS__", json("Component Catalog")?),
("__PREVIEW_FAMILY__", json(&selected.family)?),
("__PREVIEW_VARIANT__", json(&selected.name)?),
("__GALLERY_THEME__", json(&launch.theme.replace('-', "_"))?),
("__GALLERY_CATEGORY__", json(category)?),
] {
source = source.replace(placeholder, &value);
}
for token in COMPONENT_CATALOG_COLOR_TOKENS {
let value = format!("#{:08x}", selected.tokens.colors[*token].as_rgba_hex());
source = source.replace(
&format!("__COLOR_{}__", token.to_ascii_uppercase()),
&serde_json::to_string(&value).map_err(|error| error.to_string())?,
);
}
Ok(source
.replace("__BUILTIN_OPTIONS__", "[]")
.replace("__VISUAL_DIALOG__", "false")
.replace("__VISUAL_POPOVER__", "false")
.replace("__VISUAL_COMMAND__", "false")
.replace("__VISUAL_SHEET__", "false")
.replace("__VISUAL_ALERT__", "false")
.replace("__VISUAL_MENU__", "false")
.replace("__VISUAL_TOAST__", "false")
.replace(
"__VISUAL_LOCALE__",
&serde_json::to_string(&launch.locale).map_err(|error| error.to_string())?,
)
.replace("__GALLERY_ONLY__", "true"))
} else {
Ok(story.source.to_owned())
}
}
pub(crate) fn story_source(launch: &GalleryLaunch) -> Result<String, String> {
let story = resolve_story(&launch.story, &launch.case)?;
let (theme_name, primary_theme) = theme_source(&launch.theme)?;
let engine = RuntimeEngine::new();
let selected = load_theme_source(engine.engine(), theme_name, primary_theme)
.map_err(|error| error.to_string())?;
materialize_story_source(story, launch, &selected)
}
fn theme_source(slug: &str) -> Result<(&'static str, &'static str), String> {
let normalized = slug.replace('-', "_");
BUNDLED_THEME_SOURCES
.iter()
.copied()
.find(|(name, _)| name.strip_suffix(".rhai") == Some(normalized.as_str()))
.ok_or_else(|| format!("unknown Gallery theme `{slug}`"))
}
fn locale_sources(locale: &str) -> Result<[(&'static str, &'static str); 3], String> {
if !matches!(locale, "en" | "zh-CN" | "ar") {
return Err(format!("unknown Gallery locale `{locale}`"));
}
Ok([
("en.rhai", EN_LOCALE),
("zh_cn.rhai", ZH_CN_LOCALE),
("ar.rhai", AR_LOCALE),
])
}
fn story_scripts(story: &StoryDefinition, source: String) -> Result<EmbeddedScriptSource, String> {
let mut modules = bundled_dependency_modules(&source, story.module_ids)?;
modules.insert(
ModuleId::parse(story.source_module).map_err(|error| error.to_string())?,
source,
);
Ok(EmbeddedScriptSource::new(modules))
}
fn bundled_module_source(id: &str) -> Option<&'static str> {
BUNDLED_COMPONENT_SOURCES_BY_ID
.iter()
.chain(BUNDLED_MOTION_SOURCES_BY_ID)
.chain(BUNDLED_CHART_SOURCES_BY_ID)
.find_map(|(candidate, source)| (*candidate == id).then_some(*source))
}
pub(crate) fn bundled_dependency_modules(
root_source: &str,
required: &[&str],
) -> Result<BTreeMap<ModuleId, String>, String> {
let mut pending = extract_imports(root_source).map_err(|error| error.to_string())?;
for id in required {
pending.insert(ModuleId::parse(*id).map_err(|error| error.to_string())?);
}
let mut modules = BTreeMap::new();
while let Some(id) = pending.pop_first() {
if modules.contains_key(&id) {
continue;
}
let source = bundled_module_source(id.as_str())
.ok_or_else(|| format!("Gallery source imports unknown bundled module `{id}`"))?;
pending.extend(extract_imports(source).map_err(|error| error.to_string())?);
modules.insert(id, source.to_owned());
}
Ok(modules)
}
fn asset(bytes: &[u8]) -> AssetData {
AssetData {
mime_type: "image/svg+xml".to_owned(),
bytes: bytes.to_vec(),
}
}
fn story_init_source(locale: &str) -> Result<String, String> {
if locale == "en" {
Ok("fn init(ctx) {}".to_owned())
} else {
Ok(format!(
"fn init(ctx) {{ ctx.set_locale({}); }}",
serde_json::to_string(locale).map_err(|error| error.to_string())?
))
}
}
#[derive(Clone, Debug)]
struct OperationsFixture {
fixture_case: String,
}
fn operations_host_ids(fixture_case: &str) -> Vec<String> {
if fixture_case == "large" {
(1..=1_000)
.map(|index| format!("edge-{index:02}"))
.collect()
} else {
let mut hosts = vec![
"edge-01".to_owned(),
"edge-02".to_owned(),
"edge-03".to_owned(),
];
if fixture_case == "failure" {
hosts.push("edge-04".to_owned());
}
hosts
}
}
fn operations_config_name(host: &str) -> String {
format!("operations_config_{host}")
}
fn operations_config_text(host: &str, channel: &str) -> String {
let port = if host == "edge-02" { 8443 } else { 443 };
format!("server {{\n host = \"{host}\"\n channel = \"{channel}\"\n port = {port}\n}}\n")
}
fn operations_payload_field<'a>(
payload: &'a BTreeMap<String, UiValue>,
name: &str,
) -> Result<&'a UiValue, String> {
payload
.get(name)
.ok_or_else(|| format!("deployment payload is missing `{name}`"))
}
fn operations_component(
runtime: &UiRuntimeState,
) -> Result<gpui_rhai::ComponentInstancePath, String> {
runtime
.component_state
.paths()
.into_iter()
.find(|path| {
runtime
.component_state
.get(path, "deployment_phase")
.is_some()
&& runtime.component_state.get(path, "deploy_target").is_some()
})
.ok_or_else(|| "Operations Workbench state is not mounted".to_owned())
}
fn set_operations_state(
runtime: &mut UiRuntimeState,
component: &gpui_rhai::ComponentInstancePath,
field: &str,
value: UiValue,
) -> Result<(), String> {
runtime
.set_component_state_from_host(component, field, value)
.map(|_| ())
.map_err(|error| error.to_string())
}
fn finish_operations_deployment(
runtime: &mut UiRuntimeState,
component: &gpui_rhai::ComponentInstancePath,
succeeded: bool,
) -> Result<(), String> {
set_operations_state(runtime, component, "confirm_open", UiValue::Bool(false))?;
set_operations_state(
runtime,
component,
"deployment_phase",
UiValue::String(if succeeded { "complete" } else { "failed" }.to_owned()),
)?;
set_operations_state(
runtime,
component,
"deployment_progress",
UiValue::Float(if succeeded { 100.0 } else { 0.0 }),
)?;
set_operations_state(runtime, component, "toast_visible", UiValue::Bool(true))?;
set_operations_state(
runtime,
component,
"page",
UiValue::String("deployments".to_owned()),
)
}
fn commit_operations_deployment(
runtime: &mut UiRuntimeState,
payload: UiValue,
fixture_case: &str,
) -> Result<(), String> {
let UiValue::Map(payload) = payload else {
return Err("deployment payload must be an object".to_owned());
};
let UiValue::String(target) = operations_payload_field(&payload, "target")? else {
return Err("deployment target must be a string".to_owned());
};
let UiValue::String(channel) = operations_payload_field(&payload, "channel")? else {
return Err("deployment channel must be a string".to_owned());
};
let UiValue::Integer(expected_revision) =
operations_payload_field(&payload, "expected_revision")?
else {
return Err("deployment expected_revision must be an integer".to_owned());
};
let UiValue::Bool(requested_failure) = operations_payload_field(&payload, "fail")? else {
return Err("deployment fail flag must be a boolean".to_owned());
};
let component = operations_component(runtime)?;
if *requested_failure || fixture_case == "failure" {
return finish_operations_deployment(runtime, &component, false);
}
let expected_revision = u64::try_from(*expected_revision)
.map_err(|_| "deployment revision cannot be negative".to_owned())?;
let name = operations_config_name(target);
let current = runtime
.native_documents
.get(&name)
.map_err(|error| error.to_string())?;
if current.revision() != expected_revision {
return finish_operations_deployment(runtime, &component, false);
}
let next = NativeTextDocument::new(
&name,
current.revision().saturating_add(1),
operations_config_text(target, channel),
)
.map_err(|error| error.to_string())?;
runtime
.replace_native_text_document_from_host(&name, next)
.map_err(|error| error.to_string())?;
finish_operations_deployment(runtime, &component, true)
}
impl ScriptViewExtension for OperationsFixture {
fn configure_engine(&self, engine: &mut RuntimeEngine) -> Result<(), String> {
let fixture_case = self.fixture_case.clone();
engine
.register_native_handler(
NativeHandlerDescriptor::new(
NativeHandlerId::parse("gallery.operations_deploy")
.map_err(|error| error.to_string())?,
BTreeMap::from([(
"click".to_owned(),
ValueSchema::Object {
fields: BTreeMap::from([
(
"target".to_owned(),
ObjectField::required(ValueSchema::string()),
),
(
"channel".to_owned(),
ObjectField::required(ValueSchema::string()),
),
(
"expected_revision".to_owned(),
ObjectField::required(ValueSchema::integer()),
),
("fail".to_owned(), ObjectField::required(ValueSchema::Bool)),
]),
allow_unknown: false,
},
)]),
)
.map_err(|error| error.to_string())?,
move |event: NativeEvent, runtime, _, _| {
commit_operations_deployment(runtime, event.payload, &fixture_case)?;
Ok(gpui_rhai::EventResponse::new().stop())
},
)
.map_err(|error| error.to_string())
}
fn configure_runtime(&self, runtime: &mut UiRuntimeState) -> Result<(), String> {
runtime
.native_collections
.register("operations_hosts", operations_hosts(&self.fixture_case)?)
.map_err(|error| error.to_string())?;
runtime
.native_collections
.register("operations_events", operations_events(&self.fixture_case)?)
.map_err(|error| error.to_string())?;
for host in operations_host_ids(&self.fixture_case) {
let name = operations_config_name(&host);
let channel = if host == "edge-02" {
"candidate"
} else {
"stable"
};
runtime
.native_documents
.register(
&name,
NativeTextDocument::new(&name, 1, operations_config_text(&host, channel))
.map_err(|error| error.to_string())?,
)
.map_err(|error| error.to_string())?;
}
runtime.native_chart_data.insert(
"operations_metrics".to_owned(),
operations_metrics(&self.fixture_case)?,
);
let capability =
CapabilityId::parse("gallery.operations").map_err(|error| error.to_string())?;
runtime
.capabilities
.register_subscription(
CapabilityDescriptor {
id: capability,
version: semver::Version::new(1, 0, 0),
methods: BTreeMap::from([(
"stream".to_owned(),
CapabilityMethod {
input: ValueSchema::Null,
output: ValueSchema::integer(),
},
)]),
},
OperationsSubscription,
)
.map_err(|error| error.to_string())?;
Ok(())
}
}
#[derive(Clone, Copy, Debug)]
struct OperationsSubscription;
impl SubscriptionCapabilityHandler for OperationsSubscription {
fn subscribe(&mut self, method: &str, input: UiValue) -> Result<SubscriptionWork, String> {
let (name, values) = match (method, input) {
("stream", UiValue::Null) => ("gpui-rhai-gallery-stream", vec![1, 2, 3, 4]),
("stream", _) => return Err("stream expects null input".to_owned()),
_ => return Err(format!("unknown operations subscription `{method}`")),
};
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::Builder::new()
.name(name.to_owned())
.spawn(move || {
for value in values {
std::thread::sleep(Duration::from_millis(45));
if sender.send(UiValue::Integer(value)).is_err() {
break;
}
}
})
.map_err(|error| error.to_string())?;
Ok(SubscriptionWork::from_receiver(receiver))
}
}
#[derive(Clone, Debug)]
struct ChartCatalogFixture {
stream: NativeChartData,
}
impl ScriptViewExtension for ChartCatalogFixture {
fn configure_engine(&self, engine: &mut RuntimeEngine) -> Result<(), String> {
let map = ChartGeoMap::from_geojson(
"demo_map",
r#"{"type":"FeatureCollection","features":[
{"type":"Feature","id":"north","properties":{"name":"north"},"geometry":{"type":"Polygon","coordinates":[[[0,5],[10,5],[10,10],[0,10],[0,5]]]}},
{"type":"Feature","id":"south","properties":{"name":"south"},"geometry":{"type":"Polygon","coordinates":[[[0,0],[10,0],[10,5],[0,5],[0,0]]]}}
]}"#,
)
.map_err(|error| error.to_string())?;
engine
.register_chart_map(map)
.map_err(|error| error.to_string())
}
fn configure_runtime(&self, runtime: &mut UiRuntimeState) -> Result<(), String> {
runtime
.register_native_chart_data("gallery_stream", self.stream.clone())
.map_err(|error| error.to_string())
}
}
fn operations_host_row(
host: impl Into<String>,
region: impl Into<String>,
version: impl Into<String>,
status: impl Into<String>,
variant: impl Into<String>,
) -> BTreeMap<String, UiValue> {
let host = host.into();
BTreeMap::from([
("id".to_owned(), UiValue::String(host.clone())),
("host".to_owned(), UiValue::String(host)),
("region".to_owned(), UiValue::String(region.into())),
("version".to_owned(), UiValue::String(version.into())),
("status".to_owned(), UiValue::String(status.into())),
("status_variant".to_owned(), UiValue::String(variant.into())),
])
}
fn operations_hosts(fixture_case: &str) -> Result<NativeCollection, String> {
let rows = if matches!(fixture_case, "loading" | "empty") {
Vec::new()
} else if fixture_case == "large" {
let regions = ["Tokyo", "Berlin", "Virginia", "Singapore"];
(1..=1_000)
.map(|index| {
let drift = index % 97 == 0;
operations_host_row(
format!("edge-{index:02}"),
regions[(index - 1) % regions.len()],
if drift { "1.7.9" } else { "1.8.4" },
if drift { "Drift" } else { "Healthy" },
if drift { "warning" } else { "success" },
)
})
.collect()
} else {
let mut rows = vec![
operations_host_row("edge-01", "Tokyo", "1.8.4", "Healthy", "success"),
operations_host_row("edge-02", "Berlin", "1.8.3", "Deploying", "accent"),
operations_host_row("edge-03", "Virginia", "1.7.9", "Drift", "warning"),
];
if fixture_case == "failure" {
rows.push(operations_host_row(
"edge-04",
"Singapore",
"unknown",
"Unreachable",
"danger",
));
}
rows
};
NativeCollection::new("id", rows).map_err(|error| error.to_string())
}
fn operations_metrics(fixture_case: &str) -> Result<NativeChartData, String> {
let count = match fixture_case {
"streaming" => 24,
"large" => 120,
_ => 5,
};
let rows = (0..count)
.map(|index| {
let minute = format!("{:02}:{:02}", 9 + index / 12, (index % 12) * 5);
let latency = 36 + i64::from((index * 7) % 17);
BTreeMap::from([
("id".to_owned(), UiValue::String(format!("m{}", index + 1))),
("minute".to_owned(), UiValue::String(minute)),
("latency".to_owned(), UiValue::Integer(latency)),
])
})
.collect::<Vec<_>>();
let dataset = ChartDataset::from_rows(
"main",
&rows,
Some("id".to_owned()),
ChartDataLimits::default(),
)
.map_err(|error| error.to_string())?;
NativeChartData::new([dataset], ChartDataLimits::default()).map_err(|error| error.to_string())
}
fn operations_events(fixture_case: &str) -> Result<NativeCollection, String> {
let rows = if matches!(fixture_case, "loading" | "empty") {
Vec::new()
} else {
let mut rows = vec![
("e1", "09:20", "Configuration verified", "Info", "neutral"),
(
"e2",
"09:18",
"edge-02 deployment started",
"Active",
"accent",
),
(
"e3",
"09:12",
"edge-03 drift detected",
"Warning",
"warning",
),
];
if fixture_case == "failure" {
rows.push(("e4", "09:22", "edge-04 is unreachable", "Failed", "danger"));
}
if fixture_case == "streaming" {
rows.push((
"e5",
"09:25",
"Metric stream revision advanced",
"Live",
"success",
));
}
rows.into_iter()
.map(|(id, time, event, severity, variant)| {
BTreeMap::from([
("id".to_owned(), UiValue::String(id.to_owned())),
("time".to_owned(), UiValue::String(time.to_owned())),
("event".to_owned(), UiValue::String(event.to_owned())),
("severity".to_owned(), UiValue::String(severity.to_owned())),
(
"severity_variant".to_owned(),
UiValue::String(variant.to_owned()),
),
])
})
.collect()
};
NativeCollection::new("id", rows).map_err(|error| error.to_string())
}
#[allow(clippy::cast_precision_loss)]
fn chart_catalog_stream(rows: usize) -> Result<NativeChartData, String> {
let values = (0..rows)
.map(|index| {
let x = index as f64;
BTreeMap::from([
("id".to_owned(), UiValue::String(format!("p{index}"))),
("x".to_owned(), UiValue::Float(x)),
(
"y".to_owned(),
UiValue::Float((x / 270.0).sin() * 20.0 + (x / 67.0).cos() * 4.0),
),
])
})
.collect::<Vec<_>>();
let dataset = ChartDataset::from_rows(
"main",
&values,
Some("id".to_owned()),
ChartDataLimits::default(),
)
.map_err(|error| error.to_string())?;
NativeChartData::new([dataset], ChartDataLimits::default()).map_err(|error| error.to_string())
}
fn build_view(
launch: &GalleryLaunch,
chart_stream: Option<NativeChartData>,
host_slots: Option<HostSlotRegistry>,
) -> Result<EmbeddedScriptView, String> {
let story = resolve_story(&launch.story, &launch.case)?;
let (theme_name, primary_theme) = theme_source(&launch.theme)?;
let locale_sources = locale_sources(&launch.locale)?;
let engine = RuntimeEngine::new();
let selected = load_theme_source(engine.engine(), theme_name, primary_theme)
.map_err(|error| error.to_string())?;
let story_source = materialize_story_source(story, launch, &selected)?;
let source = if story.fixture == Some("component-catalog") {
story_source
} else {
format!("{}\n{}\n", story_source, story_init_source(&launch.locale)?)
};
let entry = ModuleId::parse(story.source_module).map_err(|error| error.to_string())?;
let mut view =
EmbeddedScriptView::new(entry.clone(), story_scripts(story, source)?, primary_theme)
.theme_sources(
BUNDLED_THEME_SOURCES
.iter()
.filter(|(name, _)| *name != theme_name)
.map(|(name, source)| ((*name).to_owned(), (*source).to_owned())),
)
.locale_sources(
locale_sources.map(|(name, source)| (name.to_owned(), source.to_owned())),
)
.asset_sources(BUNDLED_ASSET_SOURCES.iter().map(|(path, source)| {
(
path.strip_suffix(".svg").unwrap_or(path).to_owned(),
asset(source.as_bytes()),
)
}));
if story.fixture == Some("operations") {
let manifest = AppManifest::new(entry)
.with_capability("gallery.operations", "*")
.map_err(|error| error.to_string())?;
view = view.manifest(manifest).extension(OperationsFixture {
fixture_case: operations_fixture_case(&launch.case).to_owned(),
});
} else if story.fixture == Some("chart-catalog") {
view = view.extension(ChartCatalogFixture {
stream: match chart_stream {
Some(stream) => stream,
None => chart_catalog_stream(4_096)?,
},
});
} else if story.fixture == Some("host-embedding") {
let slots = match host_slots {
Some(slots) => slots,
None => HostSlotRegistry::new()
.with_slot("resident-form", |_, _| {
Ok(gpui_rhai::gpui::div()
.size_full()
.p_4()
.child("Rust Host supplies the resident script view in the Gallery shell")
.into_any_element())
})
.map_err(|error| error.to_string())?,
};
view = view.extension(slots);
}
if story.id == "apps/operations" && launch.case == "theme-overrides" {
view = view.theme_token_overrides(ThemeTokenOverrides {
radii: BTreeMap::from([
("sm".to_owned(), gpui_rhai::Length::Pixels(4.0)),
("md".to_owned(), gpui_rhai::Length::Pixels(7.0)),
("lg".to_owned(), gpui_rhai::Length::Pixels(10.0)),
]),
..ThemeTokenOverrides::default()
});
}
Ok(view)
}
pub fn view(launch: &GalleryLaunch) -> Result<EmbeddedScriptView, String> {
build_view(launch, None, None)
}
#[doc(hidden)]
pub fn view_with_host_slots(
launch: &GalleryLaunch,
slots: HostSlotRegistry,
) -> Result<EmbeddedScriptView, String> {
build_view(launch, None, Some(slots))
}
#[doc(hidden)]
pub fn host_resident_view(launch: &GalleryLaunch) -> Result<EmbeddedScriptView, String> {
let (theme_name, primary_theme) = theme_source(&launch.theme)?;
let source = format!(
"{}\n{}\n",
HOST_RESIDENT_STORY_SOURCE,
story_init_source(&launch.locale)?
);
let entry = ModuleId::parse("stories/apps/host_resident").map_err(|error| error.to_string())?;
let mut modules =
bundled_dependency_modules(&source, &["components/input", "components/textarea"])?;
modules.insert(entry.clone(), source);
Ok(
EmbeddedScriptView::new(entry, EmbeddedScriptSource::new(modules), primary_theme)
.theme_sources(
BUNDLED_THEME_SOURCES
.iter()
.filter(|(name, _)| *name != theme_name)
.map(|(name, source)| ((*name).to_owned(), (*source).to_owned())),
)
.locale_sources(
locale_sources(&launch.locale)?
.map(|(name, source)| (name.to_owned(), source.to_owned())),
)
.asset_sources(BUNDLED_ASSET_SOURCES.iter().map(|(path, source)| {
(
path.strip_suffix(".svg").unwrap_or(path).to_owned(),
asset(source.as_bytes()),
)
})),
)
}
#[doc(hidden)]
pub fn chart_streaming_view_with_data(
stream: NativeChartData,
) -> Result<EmbeddedScriptView, String> {
build_view(
&GalleryLaunch {
story: "charts/streaming".to_owned(),
..GalleryLaunch::default()
},
Some(stream),
None,
)
}
pub fn prepare(launch: &GalleryLaunch) -> Result<PreparedScriptView, String> {
view(launch)?.prepare().map_err(|error| error.to_string())
}
pub fn run(launch: &GalleryLaunch) -> Result<(), String> {
super::gallery_app::run(launch.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_is_deterministic_and_does_not_prepare_a_window() {
assert_eq!(list_text().lines().count(), BUNDLED_STORIES.len());
assert!(list_text().starts_with(
"components/catalog\tcomponents\tbasic,forms,navigation,documents,overlays\t"
));
}
#[test]
fn every_bundled_story_case_prepares() {
for story in BUNDLED_STORIES {
for case in story.cases {
prepare(&GalleryLaunch {
story: story.id.to_owned(),
case: case.id.to_owned(),
..GalleryLaunch::default()
})
.unwrap_or_else(|error| panic!("{} / {}: {error}", story.id, case.id));
}
}
}
#[test]
fn locale_matrix_prepares_editing_documents_and_workbench() {
for locale in ["en", "zh-CN", "ar"] {
for (story, case) in [
("components/input", "basic"),
("components/catalog", "documents"),
("apps/operations", "empty"),
] {
prepare(&GalleryLaunch {
story: story.to_owned(),
case: case.to_owned(),
locale: locale.to_owned(),
..GalleryLaunch::default()
})
.unwrap_or_else(|error| panic!("{locale} / {story} / {case}: {error}"));
}
}
}
#[test]
fn invalid_story_case_theme_and_locale_are_explicit() {
let launch = GalleryLaunch {
story: "missing".to_owned(),
..GalleryLaunch::default()
};
let Err(error) = prepare(&launch) else {
panic!("missing story must fail")
};
assert!(error.contains("unknown Gallery story"));
let launch = GalleryLaunch {
case: "missing".to_owned(),
..GalleryLaunch::default()
};
let Err(error) = prepare(&launch) else {
panic!("missing case must fail")
};
assert!(error.contains("unknown case"));
let launch = GalleryLaunch {
theme: "missing".to_owned(),
..GalleryLaunch::default()
};
let Err(error) = prepare(&launch) else {
panic!("missing theme must fail")
};
assert!(error.contains("unknown Gallery theme"));
let launch = GalleryLaunch {
locale: "missing".to_owned(),
..GalleryLaunch::default()
};
let Err(error) = prepare(&launch) else {
panic!("missing locale must fail")
};
assert!(error.contains("unknown Gallery locale"));
assert_eq!(
parse_motion_preference("reduced").unwrap(),
gpui_rhai::MotionPreference::Reduced
);
assert!(parse_motion_preference("missing").is_err());
}
}