Components / display
GeoMap builder
A controlled local map with choropleth legend, selectable feature readout, and point overlays. Click selects projected geometry; captured drag/touch pans and pinch/Ctrl-wheel zooms about the pointer. Arrow keys pan, +/- zoom, Home resets, F fits, / cycle selection. Escape cancels a drag or clears selection. No handler means a read-only map.
A builder is RenderOnce. Construct and mount it in one expression.
use gpui_kit::display::GeoMap;
Reviewed in geography geography-scale.
Construct
new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self
Options
Chain onto the value.
state(state: GeoState) -> Self
viewport(viewport: GeoViewport) -> Self
selected(selected: Option<SharedString>) -> Self
animate(animate: bool) -> Self
status_text(text: impl Into<SharedString>) -> Self
on_event(handler: impl Fn(GeoEvent, &mut Window, &mut App) + 'static) -> Self
Example · geography
fn geography(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let state = crate::motion::keyed::slot::<FixtureCamera>(
&"scene.geography.camera".into(),
window.window_handle().window_id(),
cx,
);
let viewport = state.borrow().viewport;
let selected = state.borrow().selected.clone();
let data = state
.borrow_mut()
.data
.get_or_insert_with(|| fixture(GeoProjection::Equirectangular))
.clone();
let seam = state.borrow_mut().seam.get_or_insert_with(|| Rc::new(GeoData::from_geojson(
r#"{"type":"Feature","id":"seam-island","properties":{},"geometry":{"type":"Polygon","coordinates":[[[170,-20],[-170,-20],[-170,30],[170,30],[170,-20]],[[175,-5],[-175,-5],[-175,10],[175,10],[175,-5]]]}}"#,
GeoProjection::WebMercator, GeoWorldPolicy::Auto,
GeoColorDomain { minimum:0.0, maximum:40.0,minimum_label:"0 samples".into(),maximum_label:"40 samples".into(),missing_label:"Unobserved".into() },
|_,_| Ok(GeoProperties {label:"Synthetic seam island".into(),value:Some(23.0),formatted_value:"23 samples".into()}),
).expect("synthetic seam geometry"))).clone();
let camera_target = state.clone();
let value_target = state.clone();
let geometry_target = state.clone();
stack(&theme).w(px(900.0))
.child(caption(&theme,"Original synthetic geography · local geometry, no tiles or real territories"))
.child(caption(&theme,"Drag / touch to pan · pinch or Ctrl-wheel to zoom · F fits · arrows / +/- / Home · [ and ] select"))
.child(div().row().gap_token(&theme,Space::Sm)
.child(Button::new("scene.geography.camera-target").label("Retarget camera").on_click(move |window,_| {
let mut target=camera_target.borrow_mut();
target.viewport=if target.viewport.zoom>1.0 {GeoViewport::default()} else {GeoViewport {center:GeoProjected{x:0.4,y:0.48},zoom:1.8}};
window.refresh();
}))
.child(Button::new("scene.geography.value-target").label("Retarget observations").on_click(move |window,_| {
let mut target=value_target.borrow_mut();
let source=target.data.as_ref().expect("prepared fixture");
let mut features=source.features().to_vec();
let value=if features[0].value==Some(12.0) {34.0} else {12.0};
features[0].value=Some(value); features[0].formatted_value=format!("{value} samples").into();
target.data=Some(Rc::new(GeoData::new(source.projection(),features,source.points().to_vec(),source.color_domain().clone()).expect("in-domain fixture observation")));
window.refresh();
}))
.child(Button::new("scene.geography.geometry-target").label("Replace geometry").on_click(move |window,_| {
let mut target=geometry_target.borrow_mut();
let source=target.data.as_ref().expect("prepared fixture");
if source.features().iter().any(|f| f.id.as_ref()=="unobserved") {
let mut features=source.features().to_vec();features.retain(|f| f.id.as_ref()!="unobserved");
for p in features[0].polygons.iter_mut().flat_map(|p| std::iter::once(&mut p.exterior).chain(&mut p.holes)).flatten() {p.longitude+=40.0;}
let mut points=source.points().to_vec(); points.push(GeoPoint{id:"arriving".into(),label:"New overlapping sensor".into(),position:GeoPosition{longitude:-55.0,latitude:12.0}});
target.data=Some(Rc::new(GeoData::new(source.projection(),features,points,source.color_domain().clone()).expect("translated valid fixture")));
} else {target.data=Some(fixture(GeoProjection::Equirectangular));}
window.refresh();
})))
.child(div().row().items_start().gap_token(&theme,Space::Md)
.child(div().w(px(500.0)).child(GeoMap::new("scene.geography.ready","Equirectangular · controlled camera")
.state(GeoState::Ready(data)).viewport(viewport).selected(selected)
.on_event(move |event,window,_| {
match event { GeoEvent::Select(id) => state.borrow_mut().selected=id, GeoEvent::Viewport(viewport) => state.borrow_mut().viewport=viewport }
window.refresh();
})))
.child(div().w(px(320.0)).child(GeoMap::new("scene.geography.zoomed","Web Mercator · narrow, zoomed, selected")
.state(GeoState::Stale { data:fixture(GeoProjection::WebMercator), reason:"Refresh refused; verified geometry retained".into() })
.viewport(GeoViewport { center:GeoProjected{x:0.38,y:0.46},zoom:2.2 }).selected(Some("lagoon".into())))))
.child(div().row().items_start().gap_token(&theme,Space::Md)
.child(div().w(px(280.0)).child(GeoMap::new("scene.geography.loading","Loading local features")))
.child(div().w(px(280.0)).child(GeoMap::new("scene.geography.empty","Valid empty collection").state(GeoState::Empty)))
.child(div().w(px(280.0)).child(GeoMap::new("scene.geography.refused","Unsupported input").state(GeoState::Refused(GeoRefusal::AntimeridianEdge)))))
.child(GeoMap::new("scene.geography.seam","Local GeoJSON · auto world cut · fitted island with a real hole")
.viewport(seam.fit_viewport([850.0,280.0],16.0).expect("fit synthetic island"))
.selected(Some("seam-island".into())).state(GeoState::Ready(seam)))
.into_any_element()
}
pub(super) fn geography_scale(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let state = crate::motion::keyed::slot::<FixtureCamera>(
&"scene.geography.scale-state".into(),
window.window_handle().window_id(),
cx,
);
if state.borrow().data.is_none() {
state.borrow_mut().viewport = GeoViewport {
center: GeoProjected { x: 0.5, y: 0.5 },
zoom: 8.0,
};
}
let data = state
.borrow_mut()
.data
.get_or_insert_with(|| {
Rc::new(
GeoData::new(
GeoProjection::Equirectangular,
vec![],
(0..1000)
.map(|i| GeoPoint {
id: format!("sample-{i}").into(),
label: format!("Synthetic sample {i}").into(),
position: GeoPosition {
longitude: -150.0 + f64::from(i % 100) * 3.0,
latitude: -45.0 + f64::from(i / 100) * 10.0,
},
})
.collect(),
GeoColorDomain {
minimum: 0.0,
maximum: 1.0,
minimum_label: "0".into(),
maximum_label: "1".into(),
missing_label: "Unobserved".into(),
},
)
.expect("original point grid"),
)
})
.clone();
let selected = state.borrow().selected.clone();
let viewport = state.borrow().viewport;
let dense = state.clone();
stack(&theme).w(px(900.0))
.child(caption(&theme,format!("{} original local point overlays · viewport culling · six-row virtual accessible readout",data.points().len())))
.child(caption(&theme,"Scroll the readout or navigate it by keyboard; offscreen rows retain source identities, not measured bounds"))
.child(Button::new("scene.geography.scale-density").label("Load 10,000 dense points").on_click(move |window,_| {
let mut target=dense.borrow_mut();
let points=(0..10000).map(|i| GeoPoint{id:format!("sample-{i}").into(),label:format!("Synthetic sample {i}").into(),position:GeoPosition{longitude:-179.0+f64::from(i%1000)*0.358,latitude:-80.0+f64::from(i/1000)*1.6}}).collect();
let data=GeoData::new(GeoProjection::Equirectangular,vec![],points,target.data.as_ref().expect("prepared fixture").color_domain().clone()).expect("original point grid");
target.viewport=GeoViewport{center:data.projection().project(data.points()[5000].position).expect("prepared fixture"),zoom:1.0};
target.data=Some(Rc::new(data));
window.refresh();
}))
.child(GeoMap::new("scene.geography.scale","Controlled point cloud").state(GeoState::Ready(data)).viewport(viewport).selected(selected)
.on_event(move |event,window,_| {match event {GeoEvent::Select(id)=>state.borrow_mut().selected=id,GeoEvent::Viewport(v)=>state.borrow_mut().viewport=v};window.refresh();}))
.into_any_element()
}
//! Explicit continuous color encodings, kept separate from contribution levels.
use super::support::*;
use crate::display::heatmap::{ContinuousHeatCell, ContinuousHeatmap, HeatColorScale};
#[derive(Default)]
struct MatrixRevisions {
revision: usize,
direct: bool,
selected: Option<SharedString>,
}
pub(super) fn heatmap_reordering(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let state = crate::motion::keyed::slot::<MatrixRevisions>(
&"scene.heat.reorder-state".into(),
window.window_handle().window_id(),
cx,
);
let revision = state.borrow().revision % 3;
let rows = match revision {
0 => vec!["east", "west"],
1 => vec!["west", "north", "east"],
_ => vec!["east"],
};
let columns = match revision {
0 => vec!["a", "b", "c"],
1 => vec!["c", "a", "d", "b"],
_ => vec!["a", "b"],
};
let cells = rows
.iter()
.flat_map(|row| {
columns.iter().map(move |column| {
let reading = match (*row, *column) {
("east", "a") => Some(3.0),
("east", "b") => Some(12.0),
("west", "a") => Some(42.0),
("west", "c") => Some(81.0),
("north", "b") => Some(65.0),
_ => None,
};
ContinuousHeatCell::new(
format!("{row}-{column}"),
*row,
*column,
format!("{row} / {column}"),
reading,
)
})
})
.collect();
let next = state.clone();
let direct = state.clone();
let selection = state.clone();
stack(&theme)
.w(px(560.0))
.child(caption(
&theme,
"Synthetic keyed axes · reorder, enter, exit and reinsert · raw readings never tween",
))
.child(
div()
.row()
.gap_token(&theme, Space::Sm)
.child(
Button::new("scene.heat.reorder.advance")
.label("Advance axes")
.on_click(move |window, _| {
next.borrow_mut().revision += 1;
window.refresh();
}),
)
.child(
Button::new("scene.heat.reorder.direct")
.label(if state.borrow().direct {
"Enable animation"
} else {
"Direct updates"
})
.on_click(move |window, _| {
let mut s = direct.borrow_mut();
s.direct = !s.direct;
window.refresh();
}),
),
)
.child(
ContinuousHeatmap::new(
"scene.heat.reorder",
"Stable caller coordinates",
HeatColorScale::sequential([0.0, 100.0], theme.colors.canvas, theme.colors.accent)
.expect("domain"),
PlotState::Ready(cells),
)
.rows(rows)
.columns(columns)
.animate(!state.borrow().direct)
.on_current(move |id, window, _| {
selection.borrow_mut().selected = Some(id);
window.refresh();
})
.when_some(state.borrow().selected.clone(), |chart, id| {
chart.current(id)
}),
)
.into_any_element()
}
pub(super) fn continuous_heatmap_transition(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let revision = crate::motion::keyed::slot::<usize>(
&"scene.heat.transition-state".into(),
window.window_handle().window_id(),
cx,
);
let value = match *revision.borrow() % 3 {
0 => Some(-8.0),
1 => Some(32.0),
_ => None,
};
let next = revision.clone();
stack(&theme)
.w(px(440.0))
.child(caption(
&theme,
"Synthetic observations · exact latest text with animated color; missing is never zero",
))
.child(
Button::new("scene.heat.advance")
.label("Advance reading")
.on_click(move |window, _| {
*next.borrow_mut() += 1;
window.refresh();
}),
)
.child(
ContinuousHeatmap::new(
"scene.heat.live",
"Controlled measurement",
HeatColorScale::diverging(
[-8.0, 2.0, 32.0],
theme.colors.accent,
theme.colors.canvas,
theme.colors.danger,
)
.expect("domain"),
PlotState::Ready(vec![
ContinuousHeatCell::new("changing", "row", "a", "Changing reading", value),
ContinuousHeatCell::new("zero", "row", "b", "Verified zero", Some(0.0)),
]),
)
.rows(["row"])
.columns(["a", "b"])
.motion(true),
)
.into_any_element()
}
pub(super) fn continuous_heatmap(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let diverging = HeatColorScale::diverging(
[-8.0, 2.0, 32.0],
theme.colors.accent,
theme.colors.canvas,
theme.colors.danger,
)
.expect("explicit domain");
let sequential =
HeatColorScale::sequential([0.0, 100.0], theme.colors.canvas, theme.colors.success)
.expect("explicit domain");
let cells = [
("east", "a", Some(-8.0)),
("east", "b", Some(0.0)),
("east", "c", Some(2.0)),
("west", "a", Some(17.0)),
("west", "b", Some(32.0)),
("west", "c", None),
]
.into_iter()
.map(|(row, column, reading)| {
ContinuousHeatCell::new(
format!("{row}-{column}"),
row,
column,
format!("Synthetic observation {row} / {column}"),
reading,
)
})
.collect::<Vec<_>>();
stack(&theme)
.w(px(800.0))
.child(caption(
&theme,
"Synthetic measurements · explicit neutral = 2 · no observation is not zero",
))
.child(
div().w(px(720.0)).child(
ContinuousHeatmap::new(
"scene.continuous.wide",
"Diverging measurements",
diverging,
PlotState::Ready(cells.clone()),
)
.rows(["east", "west"])
.columns(["a", "b", "c"])
.current("east-c")
.on_current(|_, _, _| {}),
),
)
.child(
div().w(px(240.0)).child(
ContinuousHeatmap::new(
"scene.continuous.narrow",
"Narrow · last verified values",
diverging,
PlotState::Stale {
data: cells,
reason: "Refresh refused".into(),
},
)
.rows(["east", "west"])
.columns(["a", "b", "c"]),
),
)
.child(
div().w(px(400.0)).child(
ContinuousHeatmap::new(
"scene.continuous.sequential",
"Sequential · domain 0–100",
sequential,
PlotState::Ready(vec![
ContinuousHeatCell::new("zero", "r", "a", "Measured zero", Some(0.0)),
ContinuousHeatCell::new("middle", "r", "b", "Middle", Some(50.0)),
ContinuousHeatCell::new("high", "r", "c", "High", Some(100.0)),
]),
)
.rows(["r"])
.columns(["a", "b", "c"]),
),
)
.child(
ContinuousHeatmap::new(
"scene.continuous.invalid",
"Invalid readings are errors",
sequential,
PlotState::Ready(vec![ContinuousHeatCell::new(
"outside",
"r",
"c",
"Outside domain",
Some(101.0),
)]),
)
.rows(["r"])
.columns(["c"]),
)
.into_any_element()
}
//! Mobile list interactions, with caller-owned async state and retained rows.
use super::support::*;
use crate::interaction::{
refresh::{PullToRefresh, RefreshState},
swipe::{SwipeActions, SwipeSide},
};
use crate::overlay::sheet::{SheetAction, SheetActionState};
struct SceneRefresh(Vec<Entity<PullToRefresh>>);
impl Global for SceneRefresh {}
pub(super) fn pull_to_refresh(_window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneRefresh>() {
let states = [
("ready", RefreshState::Ready),
(
"pending",
RefreshState::Pending("Waiting for caller".into()),
),
(
"loading",
RefreshState::Loading("Refreshing fixture".into()),
),
(
"error",
RefreshState::Error("Refresh failed · retained rows".into()),
),
];
let mut views = Vec::new();
for (id, state) in states {
let scroll = gpui::ScrollHandle::new();
let view = cx.new(|_| {
PullToRefresh::new(
format!("scene.refresh.{id}"),
scroll.clone(),
"Refresh",
"Release to refresh",
)
});
view.update(cx, |refresh, cx| {
refresh.set_state(state, cx);
refresh.set_content(
Some(Rc::new(move |_, cx| {
let theme = cx.theme().clone();
div()
.id(SharedString::from(format!("scene.refresh.{id}.scroll")))
.column()
.size_full()
.overflow_y_scroll()
.track_scroll(&scroll)
.children(
[
("notes", "Notes · verified fixture"),
("draft", "Draft · verified fixture"),
("archive", "Archive · verified fixture"),
("saved", "Saved · verified fixture"),
]
.into_iter()
.map(|(key, label)| {
div()
.h(px(64.0))
.flex_none()
.p_token(&theme, Space::Sm)
.child(crate::foundation::text(
&theme,
TypeScale::Body,
label,
))
.semantic_in(
cx,
NodeSpec::new(
format!("scene.refresh.{id}.{key}"),
Role::Group,
)
.text(label),
)
}),
)
.into_any_element()
})),
cx,
);
});
views.push(view);
}
cx.set_global(SceneRefresh(views));
}
let theme = cx.theme().clone();
stack(&theme)
.w_full()
.child(caption(
&theme,
"Fixture states · refreshing never replaces the verified rows",
))
.child(
row(&theme).items_start().children(
cx.global::<SceneRefresh>()
.0
.iter()
.map(|view| div().w(px(250.0)).h(px(370.0)).child(view.clone())),
),
)
.into_any_element()
}
struct SceneSwipe(Vec<Entity<SwipeActions>>);
impl Global for SceneSwipe {}
pub(super) fn swipe_actions(_window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneSwipe>() {
let mut views = Vec::new();
for (id, side, state) in [
("closed", None, SheetActionState::Ready),
("left", Some(SwipeSide::Left), SheetActionState::Ready),
(
"right",
Some(SwipeSide::Right),
SheetActionState::Error("Deletion failed; row retained".into()),
),
(
"pending",
Some(SwipeSide::Right),
SheetActionState::Pending("Waiting for caller".into()),
),
] {
let view = cx.new(|_| SwipeActions::new(format!("scene.swipe.{id}"), "Actions"));
view.update(cx, |swipe, cx| {
let mut remove = SheetAction::new("remove", "Delete");
remove.destructive = true;
swipe.set_actions(SwipeSide::Left, vec![SheetAction::new("pin", "Pin")], cx);
swipe.set_actions(SwipeSide::Right, vec![remove], cx);
swipe.set_content(
Some(Rc::new(move |_, cx| {
let theme = cx.theme().clone();
div()
.h(px(64.0))
.column()
.justify_center()
.child(crate::foundation::text(
&theme,
TypeScale::Label,
"Project notes · fixture",
))
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Last verified content remains visible",
))
.semantic_in(
cx,
NodeSpec::new(format!("scene.swipe.{id}.content"), Role::Group)
.text("Project notes · fixture"),
)
.into_any_element()
})),
cx,
);
swipe.reveal(side, cx);
swipe.set_state(state, cx);
});
views.push(view);
}
cx.set_global(SceneSwipe(views));
}
let theme = cx.theme().clone();
stack(&theme)
.w(px(580.0))
.child(caption(
&theme,
"Reveal is not activation · physical left and right trays",
))
.children(cx.global::<SceneSwipe>().0.iter().cloned())
.into_any_element()
}
//! Frames that decide where their contents go.
use super::support::*;
pub(super) fn mobile_page(_window: &mut Window, cx: &mut App) -> AnyElement {
use crate::layout::{AppBar, PageLayout};
let theme = cx.theme().clone();
let page = |name: &'static str, keyboard: bool, safe: bool| {
let id = format!("scene.mobile-page.{name}");
let mut insets = gpui::WindowInsets::default();
if safe {
insets.safe_area.top = px(24.0);
insets.safe_area.bottom = px(20.0);
insets.safe_area.left = px(7.0);
insets.safe_area.right = px(11.0);
}
if keyboard {
insets.ime.bottom = px(140.0);
}
div()
.w(px(290.0))
.h(px(430.0))
.border_1()
.border_color(theme.colors.control_hairline)
.surface(&theme, Surface::Panel)
.child(
PageLayout::new(
id.clone(),
ScrollArea::new(format!("{id}.scroll")).child(filler(
&theme,
"Fixture page content",
12,
)),
)
.insets(insets)
.hide_footer(keyboard)
.header(
AppBar::new(format!("{id}.bar"), name).leading(
Button::new(format!("{id}.back"))
.label("Back")
.ghost()
.control_size(ControlSize::Touch)
.disabled(!safe)
.on_click(|_, _| {}),
),
)
.footer(div().p(px(theme.space(Space::Sm))).child("Caller footer")),
)
};
stack(&theme)
.child(caption(&theme, "Fixture residual insets: desktop zero · safe area · overlapping IME with footer unmounted"))
.child(row(&theme).items_start().child(page("Desktop", false, false))
.child(page("Safe area", false, true)).child(page("Keyboard", true, true)))
.into_any_element()
}
pub(super) fn scroll_edge_effect(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(620.0))
.child(caption(
&theme,
"Fixture transcript: content scrolls beneath a Regular glass toolbar",
))
.child(
div()
.relative()
.h(px(240.0))
.overflow_hidden()
.child(
crate::layout::ScrollEdgeEffect::new("scene.scroll-edge.soft")
.top(true)
.soft()
.band(64.0)
.child(filler(&theme, "Earlier messages beneath the toolbar", 10)),
)
.child(
div().absolute().top(px(8.0)).left(px(16.0)).child(
Glass::new("scene.scroll-edge.toolbar")
.radius(Radius::Pill)
.child(
div()
.px(px(20.0))
.py(px(10.0))
.child("Transcript · fixture"),
),
),
),
)
.child(caption(
&theme,
"Reduced transparency: opaque scroll-edge backing",
))
.child(
div().h(px(160.0)).overflow_hidden().child(
crate::layout::ScrollEdgeEffect::new("scene.scroll-edge.hard")
.top(true)
.hard()
.band(64.0)
.child(filler(&theme, "The same transcript", 7)),
),
)
.into_any_element()
}
pub(super) fn grid(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let item = |id: &'static str, label: &'static str| {
GridItem::new(
id,
Card::new()
.id(format!("scene.grid.card.{id}"))
.padding(Space::Md)
.child(crate::foundation::text(&theme, TypeScale::Label, label))
.child(caption(&theme, "Caller-owned content")),
)
.span_at(Breakpoint::Large, if id == "summary" { 2 } else { 1 })
};
stack(&theme)
.w(px(840.0))
.child(caption(
&theme,
"Columns and spans resolve from the grid's measured width while semantic order stays source order",
))
.child(
Grid::new("scene.grid")
.columns(1)
.columns_at(Breakpoint::Medium, 2)
.columns_at(Breakpoint::Large, 3)
.gap(Space::Sm)
.items([
item("summary", "Summary"),
item("settings", "Settings"),
item("history", "History"),
item("policy", "Policy"),
]),
)
.into_any_element()
}
pub(super) fn container(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.child(caption(
&theme,
"Page width and gutters come from the installed theme, not component-local pixels",
))
.child(
Container::new("scene.container.readable")
.width(ContainerWidth::Readable)
.child(
Card::new()
.id("scene.container.readable.card")
.padding(Space::Md)
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Readable content",
)),
),
)
.child(
Container::new("scene.container.dialog")
.width(ContainerWidth::Dialog)
.child(
Card::new()
.id("scene.container.dialog.card")
.padding(Space::Md)
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Dialog-width content",
)),
),
)
.into_any_element()
}
/// A pane with a heading and something under it worth resizing.
///
/// Two panes of the same fixture copy demonstrate a divider and nothing else:
/// what a reader has to be able to judge is that the two sides hold different
/// things and that dragging trades room between them, so each side is given
/// its own kind of content and its own heading.
fn pane(theme: &Theme, title: &'static str, rows: &'static [&'static str]) -> gpui::Div {
div()
.column()
.size_full()
.min_w_0()
.overflow_hidden()
.child(
div()
.row()
.items_center()
.px(px(theme.space(Space::Md)))
.py(px(theme.space(Space::Sm)))
.child(
crate::foundation::text(theme, TypeScale::Label, title)
.text_tone(theme, TextTone::Muted),
),
)
.child(
div()
.column()
.gap(px(theme.space(Space::Xs)))
.px(px(theme.space(Space::Md)))
.py(px(theme.space(Space::Sm)))
.children(
rows.iter()
.map(|row| crate::foundation::text(theme, TypeScale::Body, *row)),
),
)
}
pub(super) fn split_pane(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(620.0))
.h(px(380.0))
.child(
div()
.h(px(320.0))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
SplitPane::new("scene.split.workspace")
.horizontal()
.ratio(0.34)
.min_sizes(120.0, 200.0)
.collapsible(true)
.handle_label("Resize the file tree")
.start(pane(
&theme,
"Files",
&[
"src/",
" main.rs",
" theme.rs",
" layout/",
" split.rs",
" scroll.rs",
"Cargo.toml",
],
))
.end(pane(
&theme,
"split.rs",
&[
"pub struct SplitPane {",
" ratio: f32,",
" min_start: f32,",
" min_end: f32,",
"}",
"",
"// The divider reports the ratio a drag",
"// asked for; the caller decides it.",
],
))
.on_resize(|_, _, _| {})
.on_collapse(|_, _, _| {}),
),
)
.into_any_element()
}
/// The same region, already scrolled, so the shadow that says there is content
/// above the fold is in a captured image rather than only in a test.
pub(super) fn scroll_shadow(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
crate::layout::scroll_to(
"scene.scroll.scrolled",
gpui::point(px(0.0), px(120.0)),
window,
cx,
);
stack(&theme)
.w(px(480.0))
.child(caption(
&theme,
"Scrolled to the middle: content is hidden past both ends, so both \
fade",
))
.child(
div()
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
ScrollArea::new("scene.scroll.scrolled")
.label("Run output")
.vertical()
.height(200.0)
.child(filler(&theme, "Output", 20)),
),
)
.into_any_element()
}
pub(super) fn scroll_area(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(480.0))
.child(caption(
&theme,
"Overflowing: a scrollbar, and the edges fade",
))
.child(
div()
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
ScrollArea::new("scene.scroll.output")
.label("Run output")
.vertical()
.height(200.0)
.child(filler(&theme, "Output", 20)),
),
)
// Nothing overflows here, so no scrollbar is drawn or published and
// no edge fades. The area is as tall as what it holds rather than a
// height guessed at and hoped to be enough: a box a few pixels short
// of its content draws the scrollbar and the fade of the card above
// while claiming to be the card that does neither.
.child(caption(&theme, "Fits: no scrollbar, no fade"))
.child(
div()
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
ScrollArea::new("scene.scroll.summary")
.label("Summary")
.vertical()
.fit_height()
.child(filler(&theme, "Summary", 4)),
),
)
.into_any_element()
}
/// A region scrolled off both ends, so the fade that says there is more in
/// either direction is in a captured image, beside one that hides nothing and
/// therefore fades at neither edge.
pub(super) fn scroll_fade(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
crate::layout::scroll_to(
"scene.fade.output",
gpui::point(px(0.0), px(120.0)),
window,
cx,
);
stack(&theme)
.w(px(480.0))
.child(caption(&theme, "Scrolled: content runs past both ends"))
.child(
div()
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
ScrollFade::new("scene.fade.scrolled")
.edges(FadeEdges::vertical())
.fit_height()
.child(
ScrollArea::new("scene.fade.output")
.label("Run output")
.vertical()
.height(200.0)
.child(filler(&theme, "Output", 20)),
),
),
)
// Nothing is hidden here, so no edge fades and the caller says so.
.child(caption(&theme, "Nothing hidden: no edge fades"))
.child(
div()
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
ScrollFade::new("scene.fade.settled").fit_height().child(
ScrollArea::new("scene.fade.summary")
.label("Summary")
.vertical()
.height(200.0)
.child(filler(&theme, "Summary", 7)),
),
),
)
.into_any_element()
}
/// A complete client titlebar: product content remains clickable inside the
/// drag strip, and platform controls keep their native hit-test identities.
pub(super) fn desktop_titlebar(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(720.0))
.child(
div()
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
DesktopTitlebar::new("scene.desktop-titlebar", "Workspace")
.subtitle("main.rs")
.left(
Button::new("scene.desktop-titlebar.workspace")
.label("Workspace menu")
.ghost()
.small()
.on_click(|_, _| {}),
)
.right(
Badge::new("Connected")
.success()
.id("scene.desktop-titlebar.status"),
)
.on_event(|_, _, _| {}),
)
.child(div().h(px(112.0)).p_token(&theme, Space::Lg).child(caption(
&theme,
"Host content is client input; maximize remains native Snap chrome.",
))),
)
.into_any_element()
}
/// The overflow menu of the toolbar scene, kept across frames.
pub(super) struct SceneToolbar {
overflow: Entity<Menu>,
}
impl Global for SceneToolbar {}
pub(super) fn toolbar(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneToolbar>() {
let overflow = cx.new(|cx| {
Menu::new("scene.toolbar.overflow", window, cx)
.trigger_icon(Icon::List)
.trigger_name("More actions")
});
cx.set_global(SceneToolbar { overflow });
}
let overflow = cx.global::<SceneToolbar>().overflow.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(620.0))
.child(
Toolbar::new("scene.toolbar.editor")
.label("Editor actions")
.group(
"history",
[
ToolbarItem::new(
"editor.undo",
"Undo",
IconButton::new("scene.toolbar.undo", Icon::ArrowLeft, "Undo")
.ghost()
.small()
.on_click(|_, _| {}),
)
.icon(Icon::ArrowLeft)
.shortcut("cmd-z"),
ToolbarItem::new(
"editor.redo",
"Redo",
IconButton::new("scene.toolbar.redo", Icon::ArrowRight, "Redo")
.ghost()
.small()
.on_click(|_, _| {}),
)
.icon(Icon::ArrowRight),
],
)
.group(
"view",
[ToolbarItem::new(
"editor.view",
"View",
SegmentedControl::new("scene.toolbar.view")
.label("View")
.segments([
Segment::new("code", "Code"),
Segment::new("split", "Split"),
Segment::new("preview", "Preview"),
])
.selected("split")
.small()
.on_select(|_, _, _| {}),
)],
)
.spacer()
.group(
"publish",
[
ToolbarItem::new(
"editor.share",
"Share",
Button::new("scene.toolbar.share")
.label("Share")
.secondary()
.small()
.on_click(|_, _| {}),
)
.icon(Icon::Copy),
ToolbarItem::new(
"editor.publish",
"Publish",
Button::new("scene.toolbar.publish")
.label("Publish")
.primary()
.small()
.on_click(|_, _| {}),
)
.icon(Icon::ArchiveUp),
ToolbarItem::new(
"editor.archive",
"Archive",
Button::new("scene.toolbar.archive")
.label("Archive")
.secondary()
.small()
.on_click(|_, _| {}),
)
.icon(Icon::Archive)
.disabled(true),
],
)
.overflow_after(4)
.overflow_menu(overflow),
)
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"The last two actions moved into the overflow menu.",
))
.into_any_element()
}
/// A layout nested three deep, with one leaf collapsed to its rail. The tree
/// is the caller's: every divider reports the ratio it was asked for and moves
/// nothing here.
pub(super) fn split_tree(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let layout = SplitLayout::horizontal(
"workspace",
0.26,
SplitLayout::leaf(SplitPaneSpec::new("files").min_width(140.0)),
SplitLayout::horizontal(
"body",
0.74,
SplitLayout::vertical(
"editing",
0.6,
SplitLayout::leaf(SplitPaneSpec::new("editor").min_height(90.0)),
SplitLayout::leaf(SplitPaneSpec::new("terminal").min_height(70.0)),
),
// A collapsed leaf is drawn at its rail, and the divider beside it
// is not offered: a fixed extent has no ratio to trade.
SplitLayout::leaf(SplitPaneSpec::new("outline").rail(40.0).collapsed(true)),
),
);
stack(&theme)
.w(px(680.0))
.child(
div()
.h(px(340.0))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
SplitTree::new("scene.tree.workspace")
.layout(layout)
// Each pane holds enough to reach the bottom of the
// room the tree gave it: a pane drawn half empty says
// the divider above it is in the wrong place, which is
// the one thing this scene is not about.
.pane("files", filler(&theme, "Files", 13))
.pane("editor", filler(&theme, "main.rs", 7))
.pane("terminal", filler(&theme, "Terminal", 4))
// The rail is narrower than any label, so the collapsed
// leaf is drawn as the room it still holds.
.pane("outline", div())
.on_change(|_, _, _| {}),
),
)
.child(caption(
&theme,
"A divider high in the tree stops where a leaf far below it would \
run out of room.",
))
.into_any_element()
}
/// A recursive dock topology: tab stacks are leaves of the same split tree,
/// an empty stack remains available, and a collapsed nested stack keeps its
/// rail instead of flattening the caller's arrangement.
pub(super) fn dock_tree(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let topology = DockTopology::vertical(
"root",
0.78,
DockTopology::horizontal(
"workspace",
0.28,
DockTopology::Stack(DockStack::new("source", ["files", "search"]).active("files")),
DockTopology::horizontal(
"editing",
0.9,
DockTopology::stack("editor", ["main"]),
DockTopology::Stack(DockStack::new("outline", ["symbols"]).collapsed(true)),
),
),
DockTopology::stack("terminal", std::iter::empty::<&str>()),
);
stack(&theme)
.w(px(900.0))
.child(caption(
&theme,
"recursive caller topology; tab groups, empty destinations, rails, and split dividers share existing primitives",
))
.child(
div()
.h(px(600.0))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
DockTree::new("scene.dock-tree", topology)
.panels([
DockPanel::new("files", "Files")
.icon(Icon::Folder)
.content(filler(&theme, "Workspace", 12)),
DockPanel::new("search", "Search")
.icon(Icon::Magnifier)
.badge("12"),
DockPanel::new("main", "main.rs")
.icon(Icon::Document)
.content(filler(&theme, "fn main()", 15)),
DockPanel::new("symbols", "Outline").icon(Icon::List),
])
.on_event(|_, _, _| {}),
),
)
.into_any_element()
}
#[derive(Default)]
struct SceneFloatingDock {
bounds: Option<gpui::Bounds<f32>>,
saved: Option<gpui::Bounds<f32>>,
}
impl Global for SceneFloatingDock {}
pub(super) fn dock_floating(_window: &mut Window, cx: &mut App) -> AnyElement {
use crate::layout::{DockTreeEvent, FloatingDock};
if !cx.has_global::<SceneFloatingDock>() {
cx.set_global(SceneFloatingDock::default());
}
let theme = cx.theme().clone();
let bounds = cx
.global::<SceneFloatingDock>()
.bounds
.unwrap_or(gpui::Bounds::new(
gpui::point(0.36, 0.18),
gpui::size(0.48, 0.58),
));
stack(&theme).w(px(850.0))
.child(caption(&theme, "Fixture: drag the floating header or resize corner; arrow keys adjust either. Restore uses caller-saved completed gestures."))
.child(Button::new("scene.dock-floating.restore").label("Restore saved layout").on_click(|_, cx| {
let state = cx.global_mut::<SceneFloatingDock>(); state.bounds = state.saved; cx.refresh_windows();
}))
.child(div().h(px(500.0)).child(
DockTree::new("scene.dock-floating", DockTopology::stack("workspace", ["main"]))
.floating([FloatingDock::new(DockStack::new("inspector", ["details"]), bounds).expect("valid fixture")]).expect("unique fixture identities")
.panels([
DockPanel::new("main", "Workspace").content(filler(&theme, "Caller-owned background", 15)),
DockPanel::new("details", "Details").content(filler(&theme, "Floating inspector", 7)),
])
.on_event(|event, _, cx| {
match event {
DockTreeEvent::FloatingChanged { bounds, finished, .. } => {
let state = cx.global_mut::<SceneFloatingDock>(); state.bounds = Some(bounds); if finished { state.saved = Some(bounds); }
}
DockTreeEvent::FloatingCancelled { .. } => { let state = cx.global_mut::<SceneFloatingDock>(); state.bounds = state.saved; }
_ => {}
}
cx.refresh_windows();
})
)).into_any_element()
}
/// A whole application frame: panels in regions, one region collapsed to a
/// rail, one panel the host refuses, and a status bar under all of it.
pub(super) fn ide_shell(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let mut branch = AsyncValue::<SharedString, String>::ready("main@a1b2c3".into());
branch.refresh();
branch.fail_refresh("the host is unreachable".into());
div()
.column()
.w(px(900.0))
.h(px(940.0))
.bg(theme.colors.canvas)
.text_color(theme.colors.text)
.font_family(theme.typography.sans.clone())
.child(
div().flex_1().min_h(px(0.0)).child(
Dock::new("scene.shell")
.share(DockRegion::Left, 0.24)
// The bottom region holds one refused panel, and a refusal
// is a sentence rather than a document: given a third of
// the frame it reads as a region that lost its content
// instead of as a panel the host would not open.
.share(DockRegion::Bottom, 0.17)
.panel(
DockRegion::Left,
DockPanel::new("files", "Files")
.icon(Icon::Folder)
.content(filler(&theme, "Workspace", 8)),
)
.panel(
DockRegion::Left,
DockPanel::new("search", "Search")
.icon(Icon::Magnifier)
.badge("12"),
)
.active(DockRegion::Left, "files")
.panel(
DockRegion::Centre,
DockPanel::new("editor", "main.rs")
.icon(Icon::Document)
.content(filler(&theme, "fn main()", 12)),
)
.panel(
DockRegion::Right,
DockPanel::new("outline", "Outline").icon(Icon::List),
)
.panel(
DockRegion::Right,
DockPanel::new("history", "History").icon(Icon::GitBranch),
)
.collapsed(DockRegion::Right, true)
.panel(
DockRegion::Bottom,
DockPanel::new("terminal", "Terminal")
.icon(Icon::Terminal)
.content(filler(&theme, "$ cargo test", 4)),
)
.panel(
DockRegion::Bottom,
// No badge: a panel that cannot list problems cannot
// count them either, and one unavailable reason is the
// whole claim. Gluing "there is nothing here" onto it
// would report Empty and Unavailable at once.
DockPanel::new("problems", "Problems")
.icon(Icon::Danger)
.unavailable(
"The language server is not running, so problems cannot be listed.",
),
)
// The refused panel is the one on top, because a refusal
// nobody can see is a refusal nobody was told about.
.active(DockRegion::Bottom, "problems")
.on_event(|_, _, _| {}),
),
)
.child(
StatusBar::new("scene.shell.status")
.label("Workspace status")
.start([
StatusItem::text("branch", "main")
.icon(Icon::GitBranch)
.tracking(&branch),
StatusItem::state("build", "Build passing", Tone::Success),
])
.centre([StatusItem::progress("index", "Indexing the workspace")
.count(7, 12)
.state_name("loading")])
.end([
StatusItem::text("position", "Ln 42, Col 7"),
StatusItem::action("encoding", "UTF-8").on_click(|_, _| {}),
]),
)
.into_any_element()
}
pub(super) fn aspect_ratio(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
// The ratio is the subject, so the block it drives is drawn the way every
// other bounded region in the library is: a surface step, the card radius,
// and its label inside it.
let filled = |label: &'static str| {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.surface(&theme, Surface::Sunken)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
crate::foundation::text(&theme, TypeScale::Label, label)
.text_tone(&theme, TextTone::Muted),
)
};
stack(&theme)
.child(caption(&theme, "Width given, height from the ratio"))
.child(
div().w(px(320.0)).child(
AspectRatio::of("scene.aspect.wide", 16.0, 9.0)
.width_driven()
.child(filled("16 by 9")),
),
)
.child(caption(&theme, "Height given, width from the ratio"))
.child(
div().h(px(120.0)).child(
AspectRatio::new("scene.aspect.square", 1.0)
.height_driven()
.child(filled("square")),
),
)
.into_any_element()
}
/// A container that arranges itself from its own measured width.
///
/// Both arrangements are shown at once, because the point of the component is
/// that the caller decides them and neither is a fallback for the other. The
/// unmeasured first frame is a state a still image cannot hold, so it is named
/// rather than drawn.
pub(super) fn responsive(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let arrangement = move |id: &'static str| {
let theme = theme.clone();
move |size: ContainerSize, _: &mut Window, cx: &mut App| {
let wide = size.width().is_some_and(|measured| measured >= 480.0);
// The unmeasured frame is a real state and it is named rather than
// guessed, which is the whole argument the component makes.
let heading = match size.width() {
None => "Not laid out yet, so neither arrangement is chosen".to_string(),
Some(measured) if wide => format!("{measured:.0}px wide, so two columns"),
Some(measured) => format!("{measured:.0}px wide, so one column"),
};
// A card rather than a tinted rectangle, and with enough in it to
// be a column: two panels holding one word each say nothing about
// an arrangement, which is what the component is for.
let block = |label: &'static str, lines: &'static [&'static str], cx: &mut App| {
div()
.flex_1()
.min_w_0()
.column()
.gap_token(&theme, Space::Xs)
.p_token(&theme, Space::Md)
.card_surface(&theme, CardVariant::Elevated)
.child(crate::foundation::text(&theme, TypeScale::Label, label))
.children(lines.iter().map(|line| {
crate::foundation::text(&theme, TypeScale::Caption, *line)
.text_tone(&theme, TextTone::Muted)
}))
.semantic_in(
cx,
NodeSpec::new(format!("{id}.{}", label.to_lowercase()), Role::Group)
.text(label),
)
};
let panes = div()
.gap_token(&theme, Space::Sm)
.child(block(
"Settings",
&["Theme", "Density", "Reading direction"],
cx,
))
.child(block(
"Detail",
&["Studio Dark", "Comfortable", "Left to right"],
cx,
));
div()
.column()
.gap_token(&theme, Space::Sm)
.child(
crate::foundation::text(&theme, TypeScale::Caption, heading.clone())
.text_tone(&theme, TextTone::Faint),
)
.child(if wide { panes.row() } else { panes.column() })
.semantic_in(cx, NodeSpec::new(id, Role::Group).value(heading))
.into_any_element()
}
};
let theme = cx.theme().clone();
stack(&theme)
.child(caption(&theme, "Wide enough for two columns"))
.child(div().w(px(620.0)).child(Responsive::new(
"scene.responsive.wide",
arrangement("scene.responsive.wide.body"),
)))
.child(caption(&theme, "The same content, narrow"))
.child(div().w(px(320.0)).child(Responsive::new(
"scene.responsive.narrow",
arrangement("scene.responsive.narrow.body"),
)))
.into_any_element()
}
//! Time-based and dimensional media.
use super::support::*;
/// The cube the model scenes read: a glTF document with its own buffer
/// inside it, so the reader is exercised on the path a host would take and
/// nothing is fetched to draw a scene.
pub(super) const CUBE: &[u8] = include_bytes!("../../assets/models/cube.gltf");
/// A measured envelope, in the shape a host that read the samples would hand
/// over. It is written down rather than sampled, because a scene that drew a
/// different waveform every run could not be a visual baseline.
pub(super) fn scene_peaks(count: usize) -> Vec<f32> {
(0..count)
.map(|index| {
let phase = index as f32 / count as f32;
let swell = (phase * 11.0).sin() * 0.32 + (phase * 3.0).cos() * 0.28;
(0.42 + swell).clamp(0.05, 1.0)
})
.collect()
}
pub(super) fn audio_player(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(392.0))
.child(
AudioPlayer::new("scene.audio.ready")
.control_size(gpui_kit_theme::ControlSize::Touch)
.title("Release walkthrough")
.subtitle("Recorded 12 March")
.transport(
FixtureTransport::ready(240.0)
.position(72.0)
.state(TransportState::Playing)
.volume(0.7)
.buffered([BufferedRange::new(0.0, 156.0)])
.shared(),
)
.elapsed("01:12")
.remaining("-02:48")
.peaks(scene_peaks(96))
.speeds([1.0, 1.5, 2.0])
.step_seconds(10.0)
.on_event(|_, _, _| {}),
)
.child(
Divider::new()
.id("scene.audio.rule.backend")
.label("Nothing here can decode it"),
)
.child(
AudioPlayer::new("scene.audio.absent")
.title("interview.opus")
.transport(
FixtureTransport::new()
.no_backend("There is no Opus decoder on this machine.")
.shared(),
)
.on_event(|_, _, _| {}),
)
.child(
Divider::new()
.id("scene.audio.rule.none")
.label("No player connected at all"),
)
.child(
AudioPlayer::new("scene.audio.none")
.title("Standup recording")
.on_event(|_, _, _| {}),
)
.into_any_element()
}
fn walkthrough_frame(cx: &App) -> AnyElement {
let theme = cx.theme().clone();
let metric = |label: &'static str, value: &'static str| {
div()
.flex_1()
.min_w_0()
.column()
.gap_token(&theme, Space::Xs)
.p_token(&theme, Space::Sm)
.radius(&theme, Radius::Small)
.bg(theme.colors.raised)
.child(
crate::foundation::text(&theme, TypeScale::Caption, label)
.text_tone(&theme, TextTone::Muted),
)
.child(crate::foundation::text(&theme, TypeScale::Subtitle, value))
};
div()
.size_full()
.column()
.bg(theme.colors.panel)
.child(
div()
.row()
.items_center()
.gap_token(&theme, Space::Sm)
.px_token(&theme, Space::Md)
.py_token(&theme, Space::Sm)
.surface(&theme, Surface::Raised)
.child(div().size(px(7.0)).rounded_full().bg(theme.colors.success))
.child(crate::foundation::text(
&theme,
TypeScale::Label,
"Release command center",
)),
)
.child(
div()
.flex_1()
.min_h_0()
.row()
.child(
div()
.w(px(76.0))
.h_full()
.flex_none()
.column()
.gap_token(&theme, Space::Sm)
.p_token(&theme, Space::Sm)
.bg(theme.colors.sunken)
.children([16.0, 34.0, 26.0, 42.0].map(|width| {
div()
.w(px(width))
.h(px(theme.borders.thick))
.rounded_full()
.bg(theme.colors.hairline_strong)
})),
)
.child(
div()
.flex_1()
.min_w_0()
.column()
.gap_token(&theme, Space::Sm)
.p_token(&theme, Space::Md)
.child(
div()
.row()
.gap_token(&theme, Space::Sm)
.child(metric("Checks", "48 / 52"))
.child(metric("Coverage", "92%"))
.child(metric("Elapsed", "04:18")),
)
.child(
div()
.column()
.gap_token(&theme, Space::Xs)
.p_token(&theme, Space::Sm)
.radius(&theme, Radius::Small)
.surface(&theme, Surface::Raised)
.child(crate::foundation::text(
&theme,
TypeScale::Caption,
"Verification progress",
))
.child(
div()
.h(px(6.0))
.w_full()
.rounded_full()
.overflow_hidden()
.bg(theme.colors.track)
.child(
div()
.h_full()
.w(px(218.0))
.rounded_full()
.bg(theme.colors.accent),
),
),
),
),
)
.into_any_element()
}
fn walkthrough_poster(cx: &App) -> AnyElement {
let theme = cx.theme().clone();
div()
.size_full()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap_token(&theme, Space::Sm)
.bg(theme.colors.sunken)
.child(
div()
.size(px(46.0))
.flex()
.items_center()
.justify_center()
.rounded_full()
.frame(&theme, Surface::Raised, Elevation::Raised)
.child(
gpui_kit_assets::icon(Icon::Play)
.size(px(theme.control.md.icon_size))
.text_color(theme.colors.accent_strong),
),
)
.child(crate::foundation::text(
&theme,
TypeScale::Subtitle,
"Release walkthrough",
))
.child(
crate::foundation::text(&theme, TypeScale::Caption, "4 minutes · 8 chapters")
.text_tone(&theme, TextTone::Muted),
)
.into_any_element()
}
pub(super) fn video_player(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(392.0))
.child(
VideoPlayer::new("scene.video.frame")
.control_size(gpui_kit_theme::ControlSize::Touch)
.presentation(
crate::content::transport::MediaPresentation::Inline,
"Present video",
)
.presentation_refused("Fixture host: native video presentation is unavailable.")
.on_presentation_request(|_, _, _| {})
.title("Screen capture")
.transport(
FixtureTransport::ready(96.0)
.position(24.0)
.state(TransportState::Playing)
.volume(0.6)
.shared(),
)
.frame(|_, cx| Some(walkthrough_frame(cx)))
.elapsed("00:24")
.remaining("-01:12")
.on_event(|_, _, _| {}),
)
.child(
Divider::new()
.id("scene.video.rule.poster")
.label("Poster while the first frame is still unavailable"),
)
.child(
VideoPlayer::new("scene.video.poster")
.title("Release walkthrough")
.transport(FixtureTransport::ready(240.0).shared())
.poster(|_, cx| Some(walkthrough_poster(cx)))
.elapsed("00:00")
.remaining("-04:00")
.on_event(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn model_viewer(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let read = || ModelState::read(CUBE, ModelBounds::default());
stack(&theme)
.w(px(720.0))
.child(
div()
.row()
.items_start()
.gap(px(theme.spacing.lg))
.child(
div().flex_1().min_w_0().child(
ModelViewer::new("scene.model.flat")
.title("cube.gltf")
.state(read())
.orbit(0.62, 0.42)
.height(240.0)
.on_event(|_, _, _| {}),
),
)
.child(
div().flex_1().min_w_0().child(
ModelViewer::new("scene.model.wireframe")
.title("cube.gltf")
.state(read())
.shading(ModelShading::Wireframe)
.orbit(0.62, 0.42)
.height(240.0)
.on_event(|_, _, _| {}),
),
),
)
.child(
Divider::new()
.id("scene.model.rule.refused")
.label("A document past a bound"),
)
.child(
ModelViewer::new("scene.model.refused")
.title("city-block.glb")
.rejected(ModelError::TooLarge {
limit: ModelLimit::Triangles,
found: 412_930,
allowed: 24_576,
})
.height(160.0)
.on_event(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn audio_waveform(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(caption(
&theme,
"already-normalized peaks; a missing playhead draws no played split",
))
.child(
AudioWaveform::new("scene.waveform.ready")
.peaks(scene_peaks(48))
.playhead(0.38),
)
.child(AudioWaveform::new("scene.waveform.loading").state(AudioWaveformState::Loading))
.child(AudioWaveform::new("scene.waveform.empty").state(AudioWaveformState::Empty))
.child(AudioWaveform::new("scene.waveform.unavailable").state(
AudioWaveformState::Unavailable("The sampler host refused the request.".into()),
))
.child(
AudioWaveform::new("scene.waveform.error").state(AudioWaveformState::Error(
"The sampler request failed.".into(),
)),
)
.into_any_element()
}
//! One canonical rendering per component, grouped the way the components are.
//!
//! A scene is the single description of a component's states that both the
//! gallery and the audit tests consume, so a component cannot be reviewed
//! visually in one arrangement and tested in another.
//!
//! Almost every scene is an *exhibit*: it is about one component, it lives in
//! the file named after that component's family, and it is where a reader is
//! sent to review it. `xtask api check` fails when a public component has no
//! exhibit, so a component cannot be added and left unseen.
//!
//! The rest are *compositions*, and there are three of them. A composition is
//! built the way a product would build one, because components interact in
//! ways none of them shows alone. It is nobody's coverage, and [`Shows`]
//! records the difference so the catalog does not claim a component was
//! reviewed when it was only recognised inside a shell.
//!
//! What is deliberately not here is a scene per screenshot. The files below
//! are the component families, not a taxonomy invented for this module, so a
//! component and the rendering that reviews it move together.
mod support;
mod agent;
mod canvas;
mod cartesian;
mod compositions;
mod content;
mod controls;
mod data;
#[cfg(feature = "fixtures")]
mod datetime;
mod display;
mod effects;
mod flip_configuration;
mod game;
mod geography;
mod heatmap;
mod interaction;
mod layout;
mod media;
mod motion;
mod navigation;
mod overlay;
mod plot;
mod specialized;
mod structured;
use gpui::{AnyElement, App, Window};
use crate::foundation::direction::LayoutDirection;
use agent::{
agent_avatar, agent_plan, agent_roster, agent_run_canvas, agent_run_issues, approval,
artifact_preview, clarification, cost_meter, feedback_rating, offering_catalog,
permission_matrix, persona, prompt_builder, server_list, thinking, tool_call,
};
use canvas::{
canvas_regions, canvas_tools, node_graph, node_graph_layout, node_graph_motion,
node_graph_routing,
};
use cartesian::{
cartesian, cartesian_dense, cartesian_layout, cartesian_lifecycle, cartesian_linked,
cartesian_states,
};
use compositions::{motion_flip, motion_state, reading_direction};
#[cfg(all(feature = "terminal", not(target_family = "wasm")))]
use content::terminal;
use content::{
agent_document, browser_panel, code_view, conversation, conversation_growing, diff_view,
image_viewer, log_stream, markdown, outline, transport,
};
use controls::{
actions, auth_sign_in, auth_verification, button, cascader, choice, color_picker, copy_button,
dropzone, editor, editor_folding, editor_multicursor, editor_options, editor_services,
filter_bar, find_replace, form, inline_edit, input, keybinding, keymap_editor, mention_input,
multi_select, rich_text_editor, search_field, search_input, settings, settings_page, textarea,
toggle, touch_inputs, touch_pickers, touch_pickers_open, transfer_list, translation_packs,
upload_list,
};
use data::{
data_grid, data_grid_editing, deferred_drop, diagnostics_list, drag_list, drag_tree, flow,
image_list, kanban, list, masonry, table, tree, tree_grid,
};
#[cfg(feature = "fixtures")]
use datetime::{calendar, date_range, date_time, touch_dates};
use display::{
animated_number, attachment, avatar, badge, banner, bubble, card, chart, detail, divider,
empty_state, failure_panel, heatmap, icon, loading, metric_card, outcome_panel,
performance_hud, plot, progress_bar, progress_circle, rating, sparkline, stage_progress,
state_ladder, status, tag, trace, trace_time,
};
use effects::{cinematic_effects, visual_effects};
use flip_configuration::flip_configuration;
use game::game_ui;
use geography::{geography, geography_scale};
use heatmap::{continuous_heatmap, continuous_heatmap_transition, heatmap_reordering};
use interaction::{pull_to_refresh, swipe_actions};
use layout::{
aspect_ratio, container, desktop_titlebar, dock_floating, dock_tree, grid, ide_shell,
mobile_page, responsive, scroll_area, scroll_edge_effect, scroll_fade, scroll_shadow,
split_pane, split_tree, toolbar,
};
use media::{audio_player, audio_waveform, model_viewer, video_player};
use motion::{micro, motion_primitives};
use navigation::{
accordion, adaptive_navigation, anchor_list, bottom_navigation, breadcrumb, carousel,
collapsible, document_tabs, nav_back_preview, nav_stack, pagination, sidebar, sidebar_overflow,
tabs, undo_history, wizard,
};
use overlay::{
action_sheet, bottom_sheet, command_palette, context_menu, dialog, drawer, frost, glass,
glass_materials, glass_optics, hover_card, kbd, media_caption, menu, menubar,
notification_center, overlay, popover, toast, tooltip,
};
use plot::{raw_candlestick, sankey_layout, sankey_motion};
use specialized::{specialized, specialized_distribution, specialized_exploration};
use structured::{json_view, schema_form};
/// What a rendering exists to show.
///
/// The distinction is the difference between reviewing a component and
/// recognising it. A component that has only ever been seen inside a shell has
/// been recognised; nobody has looked at its states, and the catalog should
/// not claim otherwise, which is why only [`Shows::Subjects`] counts as
/// coverage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shows {
/// The components this rendering is *about*. It arranges them so their
/// states can be compared, and it is where a reader is sent to review
/// them. Everything else it draws is scaffolding.
Subjects(&'static [&'static str]),
/// An arrangement the way a product would build one, kept because
/// components interact in ways none of them shows alone. It is nobody's
/// coverage: every component it draws is reviewed somewhere else.
Composition(&'static [&'static str]),
}
impl Shows {
/// Every component named, whichever kind this is.
pub fn components(&self) -> &'static [&'static str] {
match self {
Self::Subjects(names) | Self::Composition(names) => names,
}
}
/// The components this rendering is the review of, which is empty for a
/// composition.
pub fn subjects(&self) -> &'static [&'static str] {
match self {
Self::Subjects(names) => names,
Self::Composition(_) => &[],
}
}
}
/// One canonical rendering, addressed by name.
pub struct Scene {
pub name: &'static str,
pub build: fn(&mut Window, &mut App) -> AnyElement,
/// What this rendering is for, declared rather than inferred.
///
/// It used to be read back out of the source by following every helper a
/// scene called, which answered a different question — *what types does
/// this code path touch* — and answered it as an upper bound. Three
/// unrelated scenes that shared one fixture helper reported the same seven
/// components, so "which scene shows me" was a claim no reader could rely
/// on. Declaring it makes it exact, and `xtask api check` holds the
/// declaration to what the source can actually reach.
pub shows: Shows,
}
impl std::fmt::Debug for Scene {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Scene")
.field("name", &self.name)
.field("shows", &self.shows)
.finish()
}
}
/// Every scene, in a stable order.
pub fn catalog() -> Vec<Scene> {
#[allow(unused_mut)]
let mut scenes = vec![
Scene {
name: "bottom-sheet",
build: bottom_sheet,
shows: Shows::Subjects(&["BottomSheet"]),
},
Scene {
name: "action-sheet",
build: action_sheet,
shows: Shows::Subjects(&["ActionSheet"]),
},
Scene {
name: "pull-to-refresh",
build: pull_to_refresh,
shows: Shows::Subjects(&["PullToRefresh"]),
},
Scene {
name: "swipe-actions",
build: swipe_actions,
shows: Shows::Subjects(&["SwipeActions"]),
},
Scene {
name: "button",
build: button,
shows: Shows::Subjects(&["Button"]),
},
Scene {
name: "badge",
build: badge,
shows: Shows::Subjects(&["Badge"]),
},
Scene {
name: "card",
build: card,
shows: Shows::Subjects(&["Card", "ListRow"]),
},
Scene {
name: "rating",
build: rating,
shows: Shows::Subjects(&["Rating"]),
},
Scene {
name: "bubble",
build: bubble,
shows: Shows::Subjects(&["Bubble"]),
},
Scene {
name: "status",
build: status,
shows: Shows::Subjects(&["Callout", "StatusDot", "StatusLine"]),
},
Scene {
name: "loading",
build: loading,
shows: Shows::Subjects(&[
"BarLoader",
"LoadMore",
"PulseLoader",
"RefreshVeil",
"Skeleton",
"Spinner",
]),
},
Scene {
name: "visual-effects",
build: visual_effects,
shows: Shows::Subjects(&["EffectParticles"]),
},
Scene {
name: "cinematic-effects",
build: cinematic_effects,
shows: Shows::Subjects(&["CinematicEffect"]),
},
Scene {
name: "choice",
build: choice,
shows: Shows::Subjects(&["Checkbox", "Radio", "Slider", "Switch"]),
},
Scene {
name: "multi-select",
build: multi_select,
shows: Shows::Subjects(&["MultiSelect"]),
},
Scene {
name: "transfer-list",
build: transfer_list,
shows: Shows::Subjects(&["TransferList"]),
},
Scene {
name: "input",
build: input,
shows: Shows::Subjects(&["Select", "TextInput"]),
},
Scene {
name: "touch-inputs",
build: touch_inputs,
shows: Shows::Subjects(&["TextInput", "TextArea", "PasswordInput", "OneTimeCodeInput"]),
},
Scene {
name: "touch-pickers",
build: touch_pickers,
shows: Shows::Subjects(&["Select", "Combobox", "MultiSelect"]),
},
Scene {
name: "touch-pickers-open",
build: touch_pickers_open,
shows: Shows::Subjects(&["Combobox"]),
},
Scene {
name: "textarea",
build: textarea,
shows: Shows::Subjects(&["TextArea"]),
},
Scene {
name: "editor",
build: editor,
shows: Shows::Subjects(&["Editor"]),
},
Scene {
name: "editor-multicursor",
build: editor_multicursor,
shows: Shows::Subjects(&["Editor"]),
},
Scene {
name: "editor-services",
build: editor_services,
shows: Shows::Subjects(&["Editor"]),
},
Scene {
name: "editor-folding",
build: editor_folding,
shows: Shows::Subjects(&["Editor"]),
},
Scene {
name: "editor-options",
build: editor_options,
shows: Shows::Subjects(&["Editor", "TextArea", "RichTextEditor"]),
},
Scene {
name: "mention-input",
build: mention_input,
shows: Shows::Subjects(&["MentionInput"]),
},
Scene {
name: "rich-text-editor",
build: rich_text_editor,
shows: Shows::Subjects(&["RichTextEditor"]),
},
Scene {
name: "form",
build: form,
shows: Shows::Subjects(&[
"Combobox",
"FormField",
"NumberInput",
"SegmentedControl",
"TagInput",
]),
},
Scene {
name: "auth-sign-in",
build: auth_sign_in,
shows: Shows::Subjects(&["PasswordInput"]),
},
Scene {
name: "auth-verification",
build: auth_verification,
shows: Shows::Subjects(&["OneTimeCodeInput"]),
},
Scene {
name: "actions",
build: actions,
shows: Shows::Subjects(&["ButtonGroup", "IconButton", "SplitButton"]),
},
Scene {
name: "progress-bar",
build: progress_bar,
shows: Shows::Subjects(&["ProgressBar"]),
},
Scene {
name: "state-ladder",
build: state_ladder,
shows: Shows::Subjects(&["StateView", "StaleMark"]),
},
Scene {
name: "banner",
build: banner,
shows: Shows::Subjects(&["Banner"]),
},
Scene {
name: "outcome-panel",
build: outcome_panel,
shows: Shows::Subjects(&["OutcomePanel"]),
},
Scene {
name: "stage-progress",
build: stage_progress,
shows: Shows::Subjects(&["StageProgress"]),
},
Scene {
name: "divider",
build: divider,
shows: Shows::Subjects(&["Divider"]),
},
Scene {
name: "tag",
build: tag,
shows: Shows::Subjects(&["Tag"]),
},
Scene {
name: "avatar",
build: avatar,
shows: Shows::Subjects(&["Avatar", "AvatarGroup"]),
},
Scene {
name: "empty-state",
build: empty_state,
shows: Shows::Subjects(&["EmptyState"]),
},
Scene {
name: "kbd",
build: kbd,
shows: Shows::Subjects(&["Kbd"]),
},
Scene {
name: "overlay",
build: overlay,
shows: Shows::Subjects(&["Overlay"]),
},
Scene {
name: "dialog",
build: dialog,
shows: Shows::Subjects(&["Dialog"]),
},
Scene {
name: "tooltip",
build: tooltip,
shows: Shows::Subjects(&["Tooltip"]),
},
Scene {
name: "menu",
build: menu,
shows: Shows::Subjects(&["Menu"]),
},
Scene {
name: "context-menu",
build: context_menu,
shows: Shows::Subjects(&["ContextMenu"]),
},
Scene {
name: "popover",
build: popover,
shows: Shows::Subjects(&["Popover"]),
},
Scene {
name: "command-palette",
build: command_palette,
shows: Shows::Subjects(&["CommandPalette"]),
},
Scene {
name: "toast",
build: toast,
shows: Shows::Subjects(&["ToastLayer"]),
},
Scene {
name: "tabs",
build: tabs,
shows: Shows::Subjects(&["Tabs"]),
},
Scene {
name: "carousel",
build: carousel,
shows: Shows::Subjects(&["Carousel"]),
},
Scene {
name: "nav-stack",
build: nav_stack,
shows: Shows::Subjects(&["NavStack"]),
},
Scene {
name: "accordion",
build: accordion,
shows: Shows::Subjects(&["Accordion"]),
},
Scene {
name: "breadcrumb",
build: breadcrumb,
shows: Shows::Subjects(&["Breadcrumb"]),
},
Scene {
name: "list",
build: list,
shows: Shows::Subjects(&["List"]),
},
Scene {
name: "image-list",
build: image_list,
shows: Shows::Subjects(&["ImageList"]),
},
Scene {
name: "masonry",
build: masonry,
shows: Shows::Subjects(&["Masonry"]),
},
Scene {
name: "flow",
build: flow,
shows: Shows::Subjects(&["Flow"]),
},
Scene {
name: "table",
build: table,
shows: Shows::Subjects(&["Table"]),
},
Scene {
name: "data-grid",
build: data_grid,
shows: Shows::Subjects(&["BulkBar", "DataGrid"]),
},
Scene {
name: "data-grid-editing",
build: data_grid_editing,
shows: Shows::Subjects(&["DataGrid"]),
},
Scene {
name: "tree-grid",
build: tree_grid,
shows: Shows::Subjects(&["TreeGrid"]),
},
Scene {
name: "tree",
build: tree,
shows: Shows::Subjects(&["Tree"]),
},
Scene {
name: "split-pane",
build: split_pane,
shows: Shows::Subjects(&["SplitPane"]),
},
Scene {
name: "grid",
build: grid,
shows: Shows::Subjects(&["Grid"]),
},
Scene {
name: "container",
build: container,
shows: Shows::Subjects(&["Container"]),
},
Scene {
name: "scroll-area",
build: scroll_area,
shows: Shows::Subjects(&["ScrollArea"]),
},
Scene {
name: "scroll-shadow",
build: scroll_shadow,
shows: Shows::Subjects(&["ScrollArea"]),
},
Scene {
name: "scroll-fade",
build: scroll_fade,
shows: Shows::Subjects(&["ScrollFade"]),
},
Scene {
name: "scroll-edge-effect",
build: scroll_edge_effect,
shows: Shows::Subjects(&["ScrollEdgeEffect"]),
},
Scene {
name: "frost",
build: frost,
shows: Shows::Subjects(&["Frost"]),
},
Scene {
name: "glass",
build: glass,
shows: Shows::Subjects(&["Glass"]),
},
Scene {
name: "glass-optics",
build: glass_optics,
shows: Shows::Subjects(&["Glass", "GlassGroup"]),
},
Scene {
name: "glass-materials",
build: glass_materials,
shows: Shows::Subjects(&["Glass", "GlassGroup"]),
},
Scene {
name: "media-caption",
build: media_caption,
shows: Shows::Composition(&["Button"]),
},
Scene {
name: "toolbar",
build: toolbar,
shows: Shows::Subjects(&["Toolbar"]),
},
Scene {
name: "desktop-titlebar",
build: desktop_titlebar,
shows: Shows::Subjects(&["DesktopTitlebar"]),
},
Scene {
name: "mobile-page",
build: mobile_page,
shows: Shows::Subjects(&["AppBar", "PageLayout"]),
},
Scene {
name: "bottom-navigation",
build: bottom_navigation,
shows: Shows::Subjects(&["BottomNavigation"]),
},
Scene {
name: "nav-back-preview",
build: nav_back_preview,
shows: Shows::Subjects(&["NavStack"]),
},
Scene {
name: "adaptive-navigation",
build: adaptive_navigation,
shows: Shows::Composition(&[
"Responsive",
"Sidebar",
"BottomNavigation",
"PageLayout",
"AppBar",
]),
},
Scene {
name: "sidebar",
build: sidebar,
shows: Shows::Subjects(&["Sidebar"]),
},
Scene {
name: "sidebar-overflow",
build: sidebar_overflow,
shows: Shows::Subjects(&["Sidebar"]),
},
Scene {
name: "pagination",
build: pagination,
shows: Shows::Subjects(&["Pagination"]),
},
Scene {
name: "drawer",
build: drawer,
shows: Shows::Subjects(&["Drawer"]),
},
Scene {
name: "motion-flip",
build: motion_flip,
shows: Shows::Composition(&["Button", "Card", "ListRow"]),
},
Scene {
name: "flip-configuration",
build: flip_configuration,
shows: Shows::Composition(&["Button"]),
},
Scene {
name: "motion-state",
build: motion_state,
shows: Shows::Composition(&[
"Accordion",
"Button",
"Checkbox",
"ProgressBar",
"Radio",
"SegmentedControl",
"Switch",
"Tabs",
]),
},
Scene {
name: "motion-primitives",
build: motion_primitives,
shows: Shows::Composition(&["Button", "Tabs"]),
},
Scene {
name: "animated-number",
build: animated_number,
shows: Shows::Subjects(&["AnimatedNumber"]),
},
Scene {
name: "drag-list",
build: drag_list,
shows: Shows::Subjects(&["List"]),
},
Scene {
name: "deferred-drop",
build: deferred_drop,
shows: Shows::Composition(&["List", "Tabs", "Tree", "Button"]),
},
Scene {
name: "drag-tree",
build: drag_tree,
shows: Shows::Subjects(&["Tree"]),
},
Scene {
name: "dropzone",
build: dropzone,
shows: Shows::Subjects(&["Dropzone"]),
},
Scene {
name: "wizard",
build: wizard,
shows: Shows::Subjects(&["Wizard"]),
},
Scene {
name: "undo-history",
build: undo_history,
shows: Shows::Subjects(&["UndoHistory"]),
},
Scene {
name: "settings",
build: settings,
shows: Shows::Subjects(&["SettingsList", "SettingsRow", "SettingsSection"]),
},
Scene {
name: "settings-page",
build: settings_page,
shows: Shows::Subjects(&["SettingsList"]),
},
Scene {
name: "translation-packs",
build: translation_packs,
shows: Shows::Composition(&["Card", "Button"]),
},
Scene {
name: "detail",
build: detail,
shows: Shows::Subjects(&["DescriptionList", "Timeline"]),
},
Scene {
name: "filter-bar",
build: filter_bar,
shows: Shows::Subjects(&["FilterBar"]),
},
Scene {
name: "inline-edit",
build: inline_edit,
shows: Shows::Subjects(&["InlineEdit"]),
},
Scene {
name: "progress-circle",
build: progress_circle,
shows: Shows::Subjects(&["ProgressCircle"]),
},
Scene {
name: "split-tree",
build: split_tree,
shows: Shows::Subjects(&["SplitTree"]),
},
Scene {
name: "dock-tree",
build: dock_tree,
shows: Shows::Subjects(&["DockTree"]),
},
Scene {
name: "dock-floating",
build: dock_floating,
shows: Shows::Subjects(&["DockTree"]),
},
Scene {
name: "ide-shell",
build: ide_shell,
shows: Shows::Subjects(&["Dock", "StatusBar"]),
},
Scene {
name: "keybinding",
build: keybinding,
shows: Shows::Subjects(&["KeybindingRecorder"]),
},
Scene {
name: "keymap-editor",
build: keymap_editor,
shows: Shows::Subjects(&["KeymapEditor"]),
},
Scene {
name: "markdown",
build: markdown,
shows: Shows::Subjects(&["Markdown"]),
},
Scene {
name: "agent-document",
build: agent_document,
shows: Shows::Subjects(&["AgentDocument"]),
},
Scene {
name: "agent-roster",
build: agent_roster,
shows: Shows::Subjects(&["AgentCard", "AgentGroup", "AgentRoster", "SubagentTree"]),
},
Scene {
name: "persona",
build: persona,
shows: Shows::Subjects(&["PersonaDialogue", "PersonaPortrait", "VoiceReactive"]),
},
Scene {
name: "game-ui",
build: game_ui,
shows: Shows::Subjects(&[
"AbilityBar",
"ObjectiveTracker",
"PartyRoster",
"RewardReveal",
]),
},
Scene {
name: "agent-run-canvas",
build: agent_run_canvas,
shows: Shows::Subjects(&["AgentRunCanvas"]),
},
Scene {
name: "agent-run-issues",
build: agent_run_issues,
shows: Shows::Subjects(&["AgentRunIssues"]),
},
Scene {
name: "agent-avatar",
build: agent_avatar,
shows: Shows::Subjects(&["AgentActivityLine", "AgentAvatar"]),
},
Scene {
name: "conversation",
build: conversation,
shows: Shows::Subjects(&["MessageList"]),
},
Scene {
name: "outline",
build: outline,
shows: Shows::Subjects(&["Outline"]),
},
Scene {
name: "conversation-growing",
build: conversation_growing,
shows: Shows::Subjects(&["MessageList"]),
},
Scene {
name: "image-viewer",
build: image_viewer,
shows: Shows::Subjects(&["ImageViewer"]),
},
Scene {
name: "transport",
build: transport,
shows: Shows::Subjects(&["TransportBar"]),
},
Scene {
name: "audio-player",
build: audio_player,
shows: Shows::Subjects(&["AudioPlayer"]),
},
Scene {
name: "audio-waveform",
build: audio_waveform,
shows: Shows::Subjects(&["AudioWaveform"]),
},
Scene {
name: "video-player",
build: video_player,
shows: Shows::Subjects(&["VideoPlayer"]),
},
Scene {
name: "model-viewer",
build: model_viewer,
shows: Shows::Subjects(&["ModelViewer"]),
},
Scene {
name: "approval",
build: approval,
shows: Shows::Subjects(&["ApprovalPrompt"]),
},
Scene {
name: "clarification",
build: clarification,
shows: Shows::Subjects(&["ClarificationPanel"]),
},
Scene {
name: "permission-matrix",
build: permission_matrix,
shows: Shows::Subjects(&["PermissionMatrix"]),
},
Scene {
name: "cost-meter",
build: cost_meter,
shows: Shows::Subjects(&["ContextGauge", "CostMeter"]),
},
Scene {
name: "prompt-builder",
build: prompt_builder,
shows: Shows::Subjects(&["PromptBuilder"]),
},
Scene {
name: "feedback-rating",
build: feedback_rating,
shows: Shows::Subjects(&["FeedbackRating"]),
},
Scene {
name: "artifact-preview",
build: artifact_preview,
shows: Shows::Subjects(&["ArtifactPreview"]),
},
Scene {
name: "tool-call",
build: tool_call,
shows: Shows::Subjects(&["ToolCall"]),
},
Scene {
name: "agent-plan",
build: agent_plan,
shows: Shows::Subjects(&["AgentPlan"]),
},
Scene {
name: "node-graph",
build: node_graph,
shows: Shows::Subjects(&["GraphNode", "NodeGraph"]),
},
Scene {
name: "node-graph-layout",
build: node_graph_layout,
shows: Shows::Subjects(&["GraphNode", "NodeGraph"]),
},
Scene {
name: "node-graph-routing",
build: node_graph_routing,
shows: Shows::Subjects(&["GraphNode", "NodeGraph"]),
},
Scene {
name: "node-graph-motion",
build: node_graph_motion,
shows: Shows::Subjects(&["GraphNode", "NodeGraph"]),
},
Scene {
name: "canvas-tools",
build: canvas_tools,
shows: Shows::Subjects(&["CanvasToolbar", "Minimap", "NodeGroup"]),
},
Scene {
name: "canvas-regions",
build: canvas_regions,
shows: Shows::Subjects(&["NodeGraph"]),
},
Scene {
name: "browser-panel",
build: browser_panel,
shows: Shows::Subjects(&["BrowserPanel"]),
},
Scene {
name: "thinking",
build: thinking,
shows: Shows::Subjects(&["ThinkingBlock"]),
},
Scene {
name: "json-view",
build: json_view,
shows: Shows::Subjects(&["JsonView"]),
},
Scene {
name: "schema-form",
build: schema_form,
shows: Shows::Subjects(&["SchemaForm"]),
},
Scene {
name: "server-list",
build: server_list,
shows: Shows::Subjects(&["ServerList"]),
},
Scene {
name: "offering-catalog",
build: offering_catalog,
shows: Shows::Subjects(&["OfferingCatalog"]),
},
Scene {
name: "reading-direction",
build: reading_direction,
shows: Shows::Composition(&[
"Accordion",
"Breadcrumb",
"Button",
"Card",
"Icon",
"JsonView",
"Tabs",
"Tree",
]),
},
Scene {
name: "toggle",
build: toggle,
shows: Shows::Subjects(&["Toggle", "ToggleGroup"]),
},
Scene {
name: "collapsible",
build: collapsible,
shows: Shows::Subjects(&["Collapsible"]),
},
Scene {
name: "hover-card",
build: hover_card,
shows: Shows::Subjects(&["HoverCard"]),
},
Scene {
name: "menubar",
build: menubar,
shows: Shows::Subjects(&["Menubar"]),
},
Scene {
name: "copy-button",
build: copy_button,
shows: Shows::Subjects(&["CopyButton"]),
},
Scene {
name: "aspect-ratio",
build: aspect_ratio,
shows: Shows::Subjects(&["AspectRatio"]),
},
Scene {
name: "responsive",
build: responsive,
shows: Shows::Subjects(&["Responsive"]),
},
Scene {
name: "icon",
build: icon,
shows: Shows::Subjects(&["Icon"]),
},
Scene {
name: "document-tabs",
build: document_tabs,
shows: Shows::Subjects(&["Tabs"]),
},
Scene {
name: "search-input",
build: search_input,
shows: Shows::Subjects(&["SearchInput"]),
},
Scene {
name: "search-field",
build: search_field,
shows: Shows::Subjects(&["HighlightedText", "SearchField"]),
},
Scene {
name: "find-replace",
build: find_replace,
shows: Shows::Subjects(&["FindReplace"]),
},
Scene {
name: "notification-center",
build: notification_center,
shows: Shows::Subjects(&["NotificationCenter"]),
},
Scene {
name: "failure-panel",
build: failure_panel,
shows: Shows::Subjects(&["FailurePanel"]),
},
Scene {
name: "log-stream",
build: log_stream,
shows: Shows::Subjects(&["LogStream"]),
},
Scene {
name: "diff-view",
build: diff_view,
shows: Shows::Subjects(&["DiffView"]),
},
Scene {
name: "sparkline",
build: sparkline,
shows: Shows::Subjects(&["Sparkline"]),
},
Scene {
name: "performance-hud",
build: performance_hud,
shows: Shows::Subjects(&["PerformanceHud"]),
},
Scene {
name: "chart",
build: chart,
shows: Shows::Subjects(&[
"AreaChart",
"BarChart",
"ChartLegend",
"GaugeChart",
"LineChart",
"PieChart",
"RadarChart",
"ScatterChart",
"StackedBarChart",
]),
},
Scene {
name: "cartesian",
build: cartesian,
shows: Shows::Subjects(&["CartesianChart"]),
},
Scene {
name: "cartesian-linked",
build: cartesian_linked,
shows: Shows::Subjects(&["CartesianChart"]),
},
Scene {
name: "cartesian-layout",
build: cartesian_layout,
shows: Shows::Subjects(&["CartesianChart"]),
},
Scene {
name: "cartesian-lifecycle",
build: cartesian_lifecycle,
shows: Shows::Subjects(&["CartesianChart"]),
},
Scene {
name: "cartesian-dense",
build: cartesian_dense,
shows: Shows::Subjects(&["CartesianChart"]),
},
Scene {
name: "cartesian-states",
build: cartesian_states,
shows: Shows::Subjects(&["CartesianChart", "PieChart", "RadarChart", "GaugeChart"]),
},
Scene {
name: "plot",
build: plot,
shows: Shows::Subjects(&["CandlestickChart", "Plot", "SankeyChart"]),
},
Scene {
name: "specialized",
build: specialized,
shows: Shows::Subjects(&["SpecializedChart"]),
},
Scene {
name: "specialized-distribution",
build: specialized_distribution,
shows: Shows::Subjects(&["SpecializedChart"]),
},
Scene {
name: "specialized-exploration",
build: specialized_exploration,
shows: Shows::Subjects(&["SpecializedChart"]),
},
Scene {
name: "continuous-heatmap-transition",
build: continuous_heatmap_transition,
shows: Shows::Subjects(&["ContinuousHeatmap"]),
},
Scene {
name: "heatmap-reordering",
build: heatmap_reordering,
shows: Shows::Subjects(&["ContinuousHeatmap"]),
},
Scene {
name: "sankey-motion",
build: sankey_motion,
shows: Shows::Subjects(&["SankeyChart"]),
},
Scene {
name: "continuous-heatmap",
build: continuous_heatmap,
shows: Shows::Subjects(&["ContinuousHeatmap"]),
},
Scene {
name: "sankey-layout",
build: sankey_layout,
shows: Shows::Subjects(&["SankeyChart"]),
},
Scene {
name: "raw-candlestick",
build: raw_candlestick,
shows: Shows::Subjects(&["CartesianChart"]),
},
Scene {
name: "geography",
build: geography,
shows: Shows::Subjects(&["GeoMap"]),
},
Scene {
name: "geography-scale",
build: geography_scale,
shows: Shows::Subjects(&["GeoMap"]),
},
Scene {
name: "attachment",
build: attachment,
shows: Shows::Subjects(&["AttachmentTile"]),
},
Scene {
name: "metric-card",
build: metric_card,
shows: Shows::Subjects(&["MetricCard"]),
},
Scene {
name: "kanban",
build: kanban,
shows: Shows::Subjects(&["KanbanBoard"]),
},
Scene {
name: "micro",
build: micro,
shows: Shows::Subjects(&["MicroMark"]),
},
Scene {
name: "trace",
build: trace,
shows: Shows::Subjects(&["SpanTimeline", "TraceView"]),
},
Scene {
name: "trace-time",
build: trace_time,
shows: Shows::Subjects(&["SpanTimeline", "TraceView"]),
},
Scene {
name: "heatmap",
build: heatmap,
shows: Shows::Subjects(&["Heatmap"]),
},
Scene {
name: "color-picker",
build: color_picker,
shows: Shows::Subjects(&["ColorPicker", "ColorSwatch"]),
},
Scene {
name: "code-view",
build: code_view,
shows: Shows::Subjects(&["CodeView"]),
},
Scene {
name: "upload-list",
build: upload_list,
shows: Shows::Subjects(&["UploadList"]),
},
Scene {
name: "cascader",
build: cascader,
shows: Shows::Subjects(&["Cascader"]),
},
Scene {
name: "anchor-list",
build: anchor_list,
shows: Shows::Subjects(&["AnchorList"]),
},
Scene {
name: "diagnostics-list",
build: diagnostics_list,
shows: Shows::Subjects(&["DiagnosticsList"]),
},
];
#[cfg(all(feature = "terminal", not(target_family = "wasm")))]
scenes.push(Scene {
name: "terminal",
build: terminal,
shows: Shows::Subjects(&["Terminal"]),
});
#[cfg(feature = "fixtures")]
scenes.extend([
Scene {
name: "calendar",
build: calendar,
shows: Shows::Subjects(&["Calendar"]),
},
Scene {
name: "date-range",
build: date_range,
shows: Shows::Subjects(&["RangePicker"]),
},
Scene {
name: "date-time",
build: date_time,
shows: Shows::Subjects(&["DateInput", "TimeInput"]),
},
Scene {
name: "touch-dates",
build: touch_dates,
shows: Shows::Subjects(&["DateInput", "TimeInput", "RangePicker"]),
},
]);
scenes
}
/// The scene registered under `name`, or `None` when nothing is.
pub fn find(name: &str) -> Option<Scene> {
catalog().into_iter().find(|scene| scene.name == name)
}
/// The reading direction a scene expects.
///
/// The direction is a global, like the theme, so a capture run that renders
/// the whole catalog into one process has to set it per scene the same way it
/// activates a theme per scene. A scene that changed it while rendering would
/// leak into whichever scene came next, and the leak would show up as a
/// changed image somewhere else entirely.
pub fn direction(name: &str) -> LayoutDirection {
match name {
"reading-direction" => LayoutDirection::RightToLeft,
_ => LayoutDirection::LeftToRight,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scene_names_are_unique_and_addressable() {
let mut names: Vec<&str> = catalog().iter().map(|scene| scene.name).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count);
assert!(find("button").is_some());
assert!(find("nothing").is_none());
}
}
//! Motion primitives exercised against a live clock, plus described motion at rest.
use super::support::*;
use crate::motion::{
CubicBezier, Easing, Keyframe, Keyframes, MotionSpec, Presence, Spring, Stagger, Transition,
};
use web_time::Instant;
const CLOCK_START: u16 = 8 * 60;
const CLOCK_END: u16 = 20 * 60;
const CLOCK_STEP: u16 = 30;
const CLOCK_TICK: Duration = Duration::from_millis(500);
const CLOCK_TRANSITION_MS: u64 = 620;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MotionDemo {
SlidingTime,
Spring,
Keyframes,
Presence,
Stagger,
}
impl MotionDemo {
fn id(self) -> &'static str {
match self {
Self::SlidingTime => "sliding-time",
Self::Spring => "spring",
Self::Keyframes => "keyframes",
Self::Presence => "presence",
Self::Stagger => "stagger",
}
}
fn from_id(id: &str) -> Option<Self> {
match id {
"sliding-time" => Some(Self::SlidingTime),
"spring" => Some(Self::Spring),
"keyframes" => Some(Self::Keyframes),
"presence" => Some(Self::Presence),
"stagger" => Some(Self::Stagger),
_ => None,
}
}
}
/// Caller-owned state for the five demonstrations.
///
/// The catalog rebuilds a scene every frame, just as an ordinary host rebuilds
/// component builders. The state therefore lives outside the element tree;
/// every animated channel beneath it still has a stable semantic identity.
#[derive(Debug)]
struct SceneMotionPrimitives {
demo: MotionDemo,
clock_minutes: u16,
clock_targets: [f32; 4],
clock_digits: [Transition<f32>; 4],
clock_playing: bool,
clock_started_at: Option<Instant>,
clock_ticks: u16,
spring_selected: usize,
spring_indicator: Transition<f32>,
present: bool,
presence: Presence,
keyframes_started_at: Option<Instant>,
stagger_generation: u64,
stagger_started_at: Option<Instant>,
}
impl Global for SceneMotionPrimitives {}
impl SceneMotionPrimitives {
fn new(theme: &Theme) -> Self {
let clock_spec = clock_spec(theme);
let spring_spec = spring_spec();
let presence_spec = presence_spec(theme);
let targets = clock_digits(CLOCK_START).map(f32::from);
Self {
demo: MotionDemo::SlidingTime,
clock_minutes: CLOCK_START,
clock_targets: targets,
clock_digits: targets.map(|digit| Transition::new(digit, clock_spec)),
clock_playing: false,
clock_started_at: None,
clock_ticks: 0,
spring_selected: 0,
spring_indicator: Transition::new(0.0, spring_spec),
present: true,
presence: Presence::visible(presence_spec, presence_spec),
keyframes_started_at: None,
stagger_generation: 0,
stagger_started_at: None,
}
}
fn select(&mut self, demo: MotionDemo, theme: &Theme) {
self.demo = demo;
// Element-retained animation state disappears when its tab leaves the
// tree. Recreate that boundary here even though the scene state itself
// is longer lived, so returning to a tab never resumes an invisible
// half-frame from the previous visit.
match demo {
MotionDemo::SlidingTime => {
for (digit, target) in self
.clock_digits
.iter_mut()
.zip(self.clock_targets.iter().copied())
{
*digit = Transition::new(target, clock_spec(theme));
}
}
MotionDemo::Spring => self.spring_indicator.snap(if self.spring_selected == 0 {
0.0
} else {
120.0
}),
MotionDemo::Keyframes => self.keyframes_started_at = None,
MotionDemo::Presence => {
let spec = presence_spec(theme);
self.presence = if self.present {
Presence::visible(spec, spec)
} else {
Presence::hidden(spec, spec)
};
}
MotionDemo::Stagger => self.stagger_started_at = None,
}
}
fn toggle_clock(&mut self, now: Instant, theme: &Theme) {
if self.clock_playing {
self.clock_playing = false;
self.clock_started_at = None;
self.clock_ticks = 0;
return;
}
if self.clock_minutes >= CLOCK_END {
self.clock_minutes = CLOCK_START;
self.clock_targets = clock_digits(CLOCK_START).map(f32::from);
for (digit, target) in self
.clock_digits
.iter_mut()
.zip(self.clock_targets.iter().copied())
{
*digit = Transition::new(target, clock_spec(theme));
}
}
self.clock_playing = true;
self.clock_started_at = Some(now);
self.clock_ticks = 0;
}
fn update_clock(&mut self, now: Instant) {
let Some(started) = self.clock_started_at else {
return;
};
let due = now
.saturating_duration_since(started)
.as_millis()
.checked_div(CLOCK_TICK.as_millis())
.unwrap_or(0) as u16;
while self.clock_ticks < due && self.clock_minutes < CLOCK_END {
self.clock_ticks += 1;
self.clock_minutes = (self.clock_minutes + CLOCK_STEP).min(CLOCK_END);
for (target, next) in self
.clock_targets
.iter_mut()
.zip(clock_digits(self.clock_minutes))
{
*target = advance_digit(*target, next);
}
}
if self.clock_minutes >= CLOCK_END {
self.clock_playing = false;
self.clock_started_at = None;
self.clock_ticks = 0;
}
}
fn sliding_time(&mut self, theme: &Theme, window: &mut Window, cx: &mut App) -> AnyElement {
let spec = clock_spec(theme);
let values: [f32; 4] = std::array::from_fn(|index| {
let mut digit = self.clock_digits[index].spec(spec);
digit.set(self.clock_targets[index]);
let value = digit.animate(window, cx);
self.clock_digits[index] = digit;
value
});
let announced = format_time(self.clock_minutes);
let play_label = if self.clock_playing { "Pause" } else { "Play" };
demo_panel(theme, "Four interrupted transitions on one clock")
.child(
div()
.row()
.items_center()
.gap(px(theme.space(Space::Xs)))
.child(rolling_digit(theme, values[0]))
.child(rolling_digit(theme, values[1]))
.child(
div()
.h(px(52.0))
.flex()
.items_center()
.mono(theme)
.text_size(px(36.0))
.child(":"),
)
.child(rolling_digit(theme, values[2]))
.child(rolling_digit(theme, values[3]))
.semantic_in(
cx,
NodeSpec::new("scene.motion.clock", Role::Status)
.text("Simulated time")
.value(announced),
),
)
.child(
Button::new("scene.motion.clock.play")
.label(play_label)
.secondary()
.on_click(|_, cx| {
let now = cx.background_executor().now();
let theme = cx.theme().clone();
cx.update_global::<SceneMotionPrimitives, ()>(|scene, _| {
scene.toggle_clock(now, &theme)
});
cx.refresh_windows();
}),
)
.into_any_element()
}
fn spring(&mut self, theme: &Theme, window: &mut Window, cx: &mut App) -> AnyElement {
let mut indicator = self.spring_indicator.spec(spring_spec());
indicator.set(if self.spring_selected == 0 {
0.0
} else {
120.0
});
let left = indicator.animate(window, cx);
self.spring_indicator = indicator;
let selected = self.spring_selected;
let segment = |id: &'static str, label: &'static str, index: usize| {
div().w(px(120.0)).child(
Button::new(id)
.label(label)
.ghost()
.selected(selected == index)
.full_width(true)
.on_click(move |_, cx| {
cx.update_global::<SceneMotionPrimitives, ()>(|scene, _| {
scene.spring_selected = index
});
cx.refresh_windows();
}),
)
};
demo_panel(theme, "Velocity survives rapid retargeting")
.child(
div()
.relative()
.w(px(240.0))
.radius(theme, Radius::Control)
.well(theme)
.child(
div()
.row()
.child(segment("scene.motion.spring.queue", "Queue", 0))
.child(segment("scene.motion.spring.timeline", "Timeline", 1)),
)
.child(
div()
.absolute()
.left(px(left))
.top_0()
.w(px(120.0))
.h(px(theme.control.md.height))
.radius(theme, Radius::Control)
.bg(theme.colors.selected)
.semantic_in(
cx,
NodeSpec::new("scene.motion.spring.indicator", Role::Status)
.text("Selected segment indicator")
.value(if selected == 0 { "Queue" } else { "Timeline" }),
),
),
)
.into_any_element()
}
fn keyframes(&mut self, theme: &Theme, window: &mut Window, cx: &mut App) -> AnyElement {
let now = cx.background_executor().now();
let started = *self.keyframes_started_at.get_or_insert(now);
let elapsed = now.saturating_duration_since(started);
let period = Duration::from_millis(1_200);
let still = cx.reduce_motion();
if !still {
window.request_animation_frame();
}
let path = activity_keyframes(theme);
let bars = [
"clone", "resolve", "compile", "test", "lint", "package", "publish",
];
demo_panel(theme, "One validated path, seven delayed playheads")
.child(
div()
.h(px(72.0))
.row()
.items_end()
.gap(px(theme.space(Space::Sm)))
.children(bars.into_iter().enumerate().map(|(index, name)| {
let delay = Duration::from_millis(index as u64 * 80);
let phase = if still || elapsed < delay {
0.0
} else {
elapsed.saturating_sub(delay).as_secs_f32() / period.as_secs_f32()
};
let value = path.sample(phase.rem_euclid(1.0));
div()
.w(px(18.0))
.h(px(18.0 + 38.0 * value))
.radius(theme, Radius::Small)
.bg(theme.colors.accent)
.opacity(0.35 + 0.65 * value)
.semantic_in(
cx,
NodeSpec::new(
format!("scene.motion.keyframes.{name}"),
Role::Status,
)
.text(format!("{name} activity"))
.value(format!("{value:.3}")),
)
})),
)
.into_any_element()
}
fn presence(&mut self, theme: &Theme, window: &mut Window, cx: &mut App) -> AnyElement {
let progress = self.presence.animate(window, cx);
let visible = self.presence.is_rendered();
let button = if self.present {
"Hide notice"
} else {
"Show notice"
};
demo_panel(theme, "Logical absence waits for the exit to finish")
.child(
div()
.h(px(76.0))
.flex()
.items_center()
.children(visible.then(|| {
div()
.w(px(360.0))
.p(px(theme.space(Space::Md)))
.radius(theme, Radius::Card)
.bg(theme.colors.accent.opacity(0.14))
.opacity(progress)
.child(crate::foundation::text(
theme,
TypeScale::Body,
"The verified result is ready to review.",
))
.semantic_in(
cx,
NodeSpec::new("scene.motion.presence.notice", Role::Status)
.text("The verified result is ready to review."),
)
})),
)
.child(
Button::new("scene.motion.presence.toggle")
.label(button)
.secondary()
.on_click(|_, cx| {
cx.update_global::<SceneMotionPrimitives, ()>(|scene, _| {
scene.present = !scene.present;
if scene.present {
scene.presence.show();
} else {
scene.presence.hide();
}
});
cx.refresh_windows();
}),
)
.into_any_element()
}
fn stagger(&mut self, theme: &Theme, window: &mut Window, cx: &mut App) -> AnyElement {
let now = cx.background_executor().now();
let started = *self.stagger_started_at.get_or_insert(now);
let elapsed = now.saturating_duration_since(started);
let spec = MotionSpec::new(360, Easing::EaseOut.curve(theme));
let stagger = Stagger::from_millis(90);
let total = stagger.total(3, spec);
let still = cx.reduce_motion();
if !still && elapsed < total {
window.request_animation_frame();
}
let rows = [
("plan", "Plan the release"),
("build", "Build the artifacts"),
("publish", "Publish the result"),
];
demo_panel(theme, "A shared clock spreads one entrance across a list")
.child(div().column().gap(px(theme.space(Space::Sm))).children(
rows.into_iter().enumerate().map(|(index, (id, label))| {
let progress = if still {
1.0
} else {
stagger.progress_at(elapsed, index, 3, spec)
};
div()
.relative()
.left(px(24.0 * (1.0 - progress)))
.w(px(360.0))
.p(px(theme.space(Space::Sm)))
.radius(theme, Radius::Control)
.well(theme)
.opacity(progress)
.child(crate::foundation::text(theme, TypeScale::Body, label))
.semantic_in(
cx,
NodeSpec::new(format!("scene.motion.stagger.{id}"), Role::Status)
.text(label)
.value(format!("replay {}", self.stagger_generation)),
)
}),
))
.child(
Button::new("scene.motion.stagger.replay")
.label("Replay")
.secondary()
.on_click(|_, cx| {
let now = cx.background_executor().now();
cx.update_global::<SceneMotionPrimitives, ()>(|scene, _| {
scene.stagger_generation += 1;
scene.stagger_started_at = Some(now);
});
cx.refresh_windows();
}),
)
.into_any_element()
}
}
/// Five interactive demonstrations of interruption, physical motion,
/// authored timelines, delayed unmount and list choreography.
pub(super) fn motion_primitives(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneMotionPrimitives>() {
cx.set_global(SceneMotionPrimitives::new(cx.theme()));
}
let now = cx.background_executor().now();
let theme = cx.theme().clone();
let selected = cx.global::<SceneMotionPrimitives>().demo;
let tabs = Tabs::new("scene.motion.tabs")
.tabs([
TabItem::new("sliding-time", "Sliding time"),
TabItem::new("spring", "Spring"),
TabItem::new("keyframes", "Keyframes"),
TabItem::new("presence", "Presence"),
TabItem::new("stagger", "Stagger"),
])
.selected(selected.id())
.on_select(|id, _, cx| {
let Some(demo) = MotionDemo::from_id(id.as_ref()) else {
return;
};
let theme = cx.theme().clone();
cx.update_global::<SceneMotionPrimitives, ()>(|scene, _| scene.select(demo, &theme));
cx.refresh_windows();
});
let body = cx.update_global::<SceneMotionPrimitives, AnyElement>(|scene, cx| {
scene.update_clock(now);
if scene.clock_playing {
window.request_animation_frame();
}
match scene.demo {
MotionDemo::SlidingTime => scene.sliding_time(&theme, window, cx),
MotionDemo::Spring => scene.spring(&theme, window, cx),
MotionDemo::Keyframes => scene.keyframes(&theme, window, cx),
MotionDemo::Presence => scene.presence(&theme, window, cx),
MotionDemo::Stagger => scene.stagger(&theme, window, cx),
}
});
stack(&theme)
.w(px(640.0))
.child(tabs)
.child(body)
.into_any_element()
}
fn clock_spec(theme: &Theme) -> MotionSpec {
MotionSpec::new(CLOCK_TRANSITION_MS, Easing::EaseInOut.curve(theme))
}
fn spring_spec() -> MotionSpec {
// `bounce = 1 - damping_ratio`, so this is the same 420ms / 0.68 policy
// used by the reference segmented indicator.
MotionSpec::sprung(Spring::perceptual(Duration::from_millis(420), 0.32))
}
fn presence_spec(theme: &Theme) -> MotionSpec {
MotionSpec::new(360, Easing::EaseInOut.curve(theme))
}
fn activity_keyframes(theme: &Theme) -> Keyframes<f32> {
let linear = MotionSpec::new(1_200, CubicBezier::new(0.0, 0.0, 1.0, 1.0));
Keyframes::new(
theme,
linear,
[
Keyframe::new(0.0, 0.0),
Keyframe::new(0.35, 1.0),
// Local keyframes attach the curve to the stop being reached. Put
// EaseOut on the 0.7 stop so the outgoing 0.35..0.7 segment has
// the same curve as the reference implementation.
Keyframe::new(0.7, 0.0).eased(Easing::EaseOut),
Keyframe::new(1.0, 0.0),
],
)
.expect("the activity path has four stops")
}
fn demo_panel(theme: &Theme, caption_text: &'static str) -> gpui::Div {
div()
.min_h(px(260.0))
.w_full()
.column()
.items_center()
.justify_center()
.gap(px(theme.space(Space::Lg)))
.p(px(theme.space(Space::Xl)))
.radius(theme, Radius::Card)
.card_surface(theme, CardVariant::Elevated)
.child(caption(theme, caption_text))
}
fn rolling_digit(theme: &Theme, value: f32) -> gpui::Div {
let whole = value.floor();
let fraction = value - whole;
let current = (whole as i32).rem_euclid(10);
let next = (current + 1).rem_euclid(10);
let glyph = |digit: i32, top: f32| {
div()
.absolute()
.left_0()
.right_0()
.top(px(top))
.h(px(52.0))
.flex()
.items_center()
.justify_center()
.child(digit.to_string())
};
div()
.relative()
.w(px(38.0))
.h(px(52.0))
.overflow_hidden()
.radius(theme, Radius::Control)
.well(theme)
.mono(theme)
.text_size(px(40.0))
.child(glyph(current, -52.0 * fraction))
.child(glyph(next, 52.0 * (1.0 - fraction)))
}
fn clock_digits(minutes: u16) -> [u8; 4] {
let hour = minutes / 60;
let minute = minutes % 60;
[
(hour / 10) as u8,
(hour % 10) as u8,
(minute / 10) as u8,
(minute % 10) as u8,
]
}
fn format_time(minutes: u16) -> String {
format!("{:02}:{:02}", minutes / 60, minutes % 60)
}
/// The next scalar congruent to `digit`, always travelling forward around the
/// decimal wheel. Thus 8 → 0 becomes 8 → 10 rather than rolling backwards.
fn advance_digit(current: f32, digit: u8) -> f32 {
let current_digit = (current.round() as i32).rem_euclid(10);
let distance = (i32::from(digit) - current_digit).rem_euclid(10);
current + distance as f32
}
pub(super) fn micro(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
// Each motion gets a cell of its own. A still frame cannot show any of
// them moving, so what the picture has to carry instead is that there are
// five of them, that they are the same size, and which name belongs to
// which mark.
let cell = |mark: MicroMark| {
div()
.w(px(96.0))
.flex_none()
.column()
.items_center()
.justify_center()
.gap_token(&theme, Space::Xs)
.py_token(&theme, Space::Md)
.radius(&theme, Radius::Card)
.well(&theme)
.child(mark)
};
stack(&theme)
.w(px(560.0))
.child(caption(
&theme,
"named functions of time; reduced motion leaves the glyph still",
))
.child(
row(&theme)
.gap_token(&theme, Space::Sm)
.child(cell(MicroMark::new(
"scene.micro.heartbeat",
Micro::Heartbeat,
"Hb",
)))
.child(cell(MicroMark::new(
"scene.micro.bounce",
Micro::Bounce,
"Bn",
)))
.child(cell(MicroMark::new(
"scene.micro.wobble",
Micro::Wobble,
"Wb",
)))
.child(cell(MicroMark::new("scene.micro.pop", Micro::Pop, "Pp")))
.child(cell(MicroMark::new(
"scene.micro.sparkle",
Micro::Sparkle,
"Sk",
))),
)
.child(caption(
&theme,
"the same functions on things that are not glyphs",
))
.child(
row(&theme)
.gap_token(&theme, Space::Sm)
.child(div().child(Badge::new("12 unread").neutral()).micro(
"scene.micro.badge",
Micro::Pop,
cx,
))
.child(div().child(Tag::new("scene.micro.tag", "fixture")).micro(
"scene.micro.tag.mark",
Micro::Wobble,
cx,
))
.child(div().child(StatusDot::new(Tone::Success)).micro(
"scene.micro.dot",
Micro::Heartbeat,
cx,
)),
)
.child(caption(
&theme,
"a described motion, sampled along its own run rather than played",
))
.child(described(&theme))
.into_any_element()
}
/// A motion written with `motion!`, laid out as the frames it passes through.
///
/// This is the one thing a still frame can say about motion: a described
/// motion can be read at a point in its run without running it, so the frames
/// are drawn side by side and the picture is the same every time. Playing it
/// is [`Animator`]'s job and needs a window; the gallery is where that is
/// looked at.
fn described(theme: &Theme) -> gpui::Div {
let arrive = crate::motion! {
duration: 420;
ease: overshoot;
opacity: 0.0 => 1.0;
y: 12.0 => 0.0;
};
let frame = |head: f32, label: &'static str| {
let sample = arrive.sample(theme, head);
div()
.w(px(96.0))
.flex_none()
.column()
.items_center()
.gap_token(theme, Space::Xs)
.py_token(theme, Space::Md)
.radius(theme, Radius::Card)
.well(theme)
// The slot stays drawn at every head. At the start of the run the
// mark has not arrived yet, and a cell with nothing in it reads
// as a frame that failed to render rather than as the frame where
// the motion has not begun.
.child(
div()
.w(px(56.0))
.h(px(24.0))
.radius(theme, Radius::Control)
.bg(theme.colors.track)
.child(
sample.apply(
div()
.size_full()
.radius(theme, Radius::Control)
.bg(theme.colors.accent),
),
),
)
.child(caption(theme, label))
};
row(theme)
.gap_token(theme, Space::Sm)
.child(frame(0.0, "start"))
.child(frame(0.25, "quarter"))
.child(frame(0.5, "half"))
.child(frame(0.75, "three quarters"))
.child(frame(1.0, "settled"))
}
//! Getting to a place, and knowing which place you are in.
use super::support::*;
pub(super) fn bottom_navigation(_window: &mut Window, cx: &mut App) -> AnyElement {
use crate::navigation::{BottomNavigation, NavigationItem};
let theme = cx.theme().clone();
stack(&theme)
.w(px(390.0))
.child(caption(
&theme,
"Fixture destinations; selected, enabled and disabled actions",
))
.child(
BottomNavigation::new("scene.bottom-navigation.ready")
.items([
NavigationItem::new("library", "Library"),
NavigationItem::new("search", "Search"),
NavigationItem::new("settings", "Settings").disabled(true),
])
.selected("search")
.on_select(|_, _, _| {}),
)
.child(caption(
&theme,
"No host handler: destinations are unavailable",
))
.child(
BottomNavigation::new("scene.bottom-navigation.unavailable")
.items([
NavigationItem::new("library", "Library"),
NavigationItem::new("search", "Search"),
])
.selected("library"),
)
.into_any_element()
}
pub(super) fn adaptive_navigation(_window: &mut Window, cx: &mut App) -> AnyElement {
use crate::layout::{AppBar, PageLayout, Responsive};
use crate::navigation::{BottomNavigation, NavigationItem};
let theme = cx.theme().clone();
let make = |id: &'static str, width: f32| {
div().w(px(width)).h(px(320.0)).child(
Responsive::new(id, move |size, _, _| {
let wide = size.at_least(600.0);
let body = div()
.flex()
.size_full()
.when(wide, |body| {
body.child(
Sidebar::new(format!("{id}.rail"))
.section(SidebarSection::new("destinations").items([
SidebarItem::new("library", "Library"),
SidebarItem::new("search", "Search"),
]))
.active("search")
.on_select(|_, _, _| {}),
)
})
.child(
div()
.flex_1()
.p(px(16.0))
.child("Fixture content · caller owns route selection"),
);
PageLayout::new(format!("{id}.page"), body)
.insets(gpui::WindowInsets::default())
.header(AppBar::new(
format!("{id}.bar"),
if wide {
"Wide container"
} else {
"Compact container"
},
))
.hide_footer(wide)
.footer(
BottomNavigation::new(format!("{id}.bottom"))
.items([
NavigationItem::new("library", "Library"),
NavigationItem::new("search", "Search"),
])
.selected("search")
.on_select(|_, _, _| {}),
)
.into_any_element()
})
.fill(),
)
};
stack(&theme)
.child(caption(&theme, "Same caller data; measured container width selects rail or bottom destinations. No platform guessing."))
.child(make("scene.adaptive-navigation.wide", 640.0))
.child(make("scene.adaptive-navigation.compact", 350.0))
.into_any_element()
}
pub(super) fn nav_back_preview(window: &mut Window, cx: &mut App) -> AnyElement {
use crate::navigation::{BackTransition, NavHistory, NavStack};
let theme = cx.theme().clone();
let state = window.use_keyed_state("scene.nav-back-preview.state", cx, |_, cx| {
let mut history = NavHistory::new("overview");
history.push("details");
(history, None::<BackTransition>, cx.focus_handle())
});
let (history, preview, focus) = state.read(cx).clone();
let progress = state.clone();
let cancel = state.clone();
let commit = state.clone();
let reset = state.clone();
let mut page = NavStack::new(
"scene.nav-back-preview",
&history,
history.current().clone(),
focus,
Card::new()
.padding(Space::Xl)
.child(history.current().clone()),
);
if let Some(preview) = &preview {
page = page.back_transition(preview);
}
stack(&theme).w(px(550.0))
.child(caption(&theme, "Fixture host-fed progress; cancel retains the page, accept mutates caller history. Reduced motion holds full opacity."))
.child(Slider::new("scene.nav-back-preview.progress").label("Back progress")
.range(0.0, 1.0).value(preview.as_ref().map_or(0.0, BackTransition::progress))
.disabled(!history.can_pop()).on_change(move |value, _, cx| progress.update(cx, |state, cx| {
if state.1.is_none() { state.1 = BackTransition::begin(&state.0); }
if let Some(preview) = &mut state.1 { preview.update(value); }
cx.notify();
})))
.child(row(&theme)
.child(Button::new("scene.nav-back-preview.cancel").label("Cancel").disabled(preview.is_none())
.on_click(move |_, cx| cancel.update(cx, |state, cx| {
if let Some(preview) = state.1.take() { preview.cancel(); } cx.notify();
})))
.child(Button::new("scene.nav-back-preview.accept").label("Accept back").disabled(preview.is_none())
.on_click(move |_, cx| commit.update(cx, |state, cx| {
if let Some(preview) = state.1.take() { preview.commit(&mut state.0); } cx.notify();
})))
.child(Button::new("scene.nav-back-preview.reset").label("Forward").disabled(!history.can_forward())
.on_click(move |_, cx| reset.update(cx, |state, cx| { state.0.forward(); cx.notify(); }))))
.child(page).into_any_element()
}
pub(super) fn nav_stack(window: &mut Window, cx: &mut App) -> AnyElement {
use crate::navigation::nav_stack::{NavHistory, NavStack};
let theme = cx.theme().clone();
let state = window.use_keyed_state("scene.nav-stack.state", cx, |_, cx| {
(
NavHistory::new("overview"),
cx.focus_handle(),
cx.focus_handle(),
cx.focus_handle(),
)
});
let (history, overview, details, replacement) = state.read(cx).clone();
let focus = match history.current().as_ref() {
"details" => details,
"replacement" => replacement,
_ => overview,
};
let back = state.clone();
let forward = state.clone();
let push = state.clone();
let replace = state.clone();
stack(&theme).w(px(620.0))
.child(caption(&theme, "Caller-owned visit history; back retains forward visits, push forks, replace preserves branches"))
.child(row(&theme)
.child(Button::new("scene.nav-stack.back").label("Back").disabled(!history.can_pop())
.on_click(move |_, cx| back.update(cx, |state, cx| { state.0.pop(); cx.notify(); })))
.child(Button::new("scene.nav-stack.forward").label("Forward").disabled(!history.can_forward())
.on_click(move |_, cx| forward.update(cx, |state, cx| { state.0.forward(); cx.notify(); })))
.child(Button::new("scene.nav-stack.push").label("Push details")
.disabled(history.entries().iter().any(|id| id.as_ref() == "details"))
.on_click(move |_, cx| push.update(cx, |state, cx| { state.0.push("details"); cx.notify(); })))
.child(Button::new("scene.nav-stack.replace").label("Replace")
.disabled(history.entries().iter().any(|id| id.as_ref() == "replacement"))
.on_click(move |_, cx| replace.update(cx, |state, cx| { state.0.replace("replacement"); cx.notify(); }))))
.child(NavStack::new("scene.nav-stack", &history, history.current().clone(), focus,
Card::new().padding(Space::Xl)
.child(crate::foundation::text(&theme, TypeScale::Title, history.current().clone()))
.child(crate::foundation::text(&theme, TypeScale::Body,
"Only the active page is mounted. Return visits restore focus without keeping inactive controls live."))))
.into_any_element()
}
pub(super) fn carousel(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let page = |id: &'static str, label: &'static str, text: &'static str| {
CarouselItem::new(
id,
label,
Card::new()
.id(format!("scene.carousel.card.{id}"))
.padding(Space::Xl)
.child(crate::foundation::text(&theme, TypeScale::Title, label))
.child(crate::foundation::text(&theme, TypeScale::Body, text)),
)
};
stack(&theme)
.w(px(620.0))
.child(caption(
&theme,
"A controlled, focusable page track with direct selection and reduced-motion-aware transitions",
))
.child(
Carousel::new("scene.carousel.ready")
.items([
page("overview", "Overview", "The first page is active."),
page("details", "Details", "The caller owns the page content."),
page("history", "History", "The track reports page intents."),
])
.active("details")
.looped(true)
.on_event(|_, _, _| {}),
)
.child(
Carousel::new("scene.carousel.empty")
.phase(Phase::Empty)
.on_event(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn tabs(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(caption(
&theme,
"In-content tabs: raised control, quiet track, tabular count",
))
.child(
Tabs::new("scene.tabs.workspace")
.tabs([
TabItem::new("overview", "Overview").icon(Icon::Widget),
TabItem::new("runs", "Runs").badge("12"),
TabItem::new("logs", "Logs"),
TabItem::new("billing", "Billing").disabled(true),
])
.selected("runs")
.on_select(|_, _, _| {}),
)
.child(caption(
&theme,
"places, not documents: Regular Liquid capsules, \
with a translucent selected fill on the material",
))
.child(
Tabs::new("scene.tabs.capsules")
.capsules()
.control_size(ControlSize::Sm)
.tabs([
TabItem::new("code", "Portal")
.icon(Icon::Terminal)
.tint(theme.colors.accent),
TabItem::new("game", "Harbour")
.icon(Icon::Play)
.tint(theme.colors.warning),
TabItem::new("design", "notes")
.icon(Icon::Pen)
.tint(theme.colors.info),
])
.selected("code")
.on_select(|_, _, _| {}),
)
// The body belongs to the caller: tabs render the strip only.
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Runs are rendered by the caller, not by the strip.",
))
.child(caption(
&theme,
"the other answer to a strip with no room: it scrolls, and the \
edge with more behind it fades",
))
// Deliberately narrower than its tabs, because a strip that fits shows
// nothing about what a strip that does not fit does.
.child(
div().w(px(300.0)).child(
Tabs::new("scene.tabs.spaces")
.tabs((1..=9).map(|n| {
TabItem::new(format!("space-{n}"), format!("Workspace {n}"))
.icon(Icon::Widget)
}))
.selected("space-5")
.scrolling()
.on_select(|_, _, _| {}),
),
)
.into_any_element()
}
/// The overflow menu the document-tab scene hangs off the strip.
///
/// The strip does not own it: a menu is an entity with an open state that
/// outlives a frame, so the caller builds it and the strip fills it.
pub(super) struct SceneDocumentTabs {
overflow: Entity<Menu>,
}
impl Global for SceneDocumentTabs {}
pub(super) fn document_tabs(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneDocumentTabs>() {
// An icon-only trigger still has to be nameable, so it carries the
// strip's own word for what moved into it.
let name = cx.strings().text(StringKey::TabMoreTabs);
let overflow = cx.new(|cx| {
Menu::new("scene.document-tabs.overflow", window, cx)
.trigger_icon(Icon::AltArrowDown)
.trigger_name(name)
});
cx.set_global(SceneDocumentTabs { overflow });
}
let overflow = cx.global::<SceneDocumentTabs>().overflow.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(620.0))
.child(caption(
&theme,
"clean, unsaved, saving, save failed — three marks, and silence for the fourth",
))
.child(
Tabs::new("scene.document-tabs.editor")
.tabs([
TabItem::new("readme", "README.md").closable(true),
TabItem::new("main", "main.rs").dirty().closable(true),
TabItem::new("theme", "theme.json").saving().closable(true),
TabItem::new("notes", "notes.md")
.save_failed("The workspace is read-only.")
.closable(true),
])
.selected("main")
.on_select(|_, _, _| {})
.on_close(|_, _, _| {}),
)
.child(caption(
&theme,
"past the declared limit the rest go to a menu, which stays reachable from the keyboard",
))
.child(
Tabs::new("scene.document-tabs.overflowing")
.tabs([
TabItem::new("one", "adapter.rs").closable(true),
TabItem::new("two", "catalog.rs").dirty().closable(true),
TabItem::new("three", "harness.rs").closable(true),
TabItem::new("four", "registry.rs").closable(true),
TabItem::new("five", "transport.rs").dirty().closable(true),
])
.selected("two")
.overflow_after(3)
.overflow_menu(overflow)
.on_select(|_, _, _| {})
.on_close(|_, _, _| {}),
)
.into_any_element()
}
pub(super) struct SceneAnchorList {
overflow: Entity<Menu>,
}
impl Global for SceneAnchorList {}
pub(super) fn anchor_list(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneAnchorList>() {
let label = cx.strings().text(StringKey::AnchorMoreSections);
let overflow = cx.new(|cx| Menu::new("scene.anchor-list.menu", window, cx).trigger(label));
cx.set_global(SceneAnchorList { overflow });
}
let theme = cx.theme().clone();
let overflow = cx.global::<SceneAnchorList>().overflow.clone();
stack(&theme)
.w(px(700.0))
.child(caption(
&theme,
"section intents only; the declared overflow moves anchors into a menu",
))
.child(
AnchorList::new("scene.anchor-list")
.anchors([
Anchor::new("summary", "Summary"),
Anchor::new("inputs", "Inputs"),
Anchor::new("constraints", "Constraints"),
Anchor::new("verification", "Verification"),
Anchor::new("history", "History").disabled(true),
])
.active("inputs")
.overflow_after(3)
.overflow_menu(overflow)
.on_navigate(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn accordion(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(
Accordion::new("scene.accordion.settings")
.expanded_ids(&["network"])
.on_toggle(|_, _, _, _| {})
.section(
AccordionSection::new("network", "Network")
.description("How this machine reaches a host")
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"Requests go out over the system proxy.",
)),
)
.section(
AccordionSection::new("storage", "Storage")
.description("Where verified results are kept")
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"Nothing is written outside the workspace.",
)),
)
.section(
AccordionSection::new("policy", "Managed by policy")
.description("This machine cannot change these")
.disabled(true)
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"Set by the administrator.",
)),
),
)
// The open section is the last one, which is the case where the body
// meets the card's rounded corners.
.child(
Accordion::new("scene.accordion.release")
.expanded_ids(&["publish"])
.on_toggle(|_, _, _, _| {})
.section(
AccordionSection::new("build", "Build")
.description("What the last run produced"),
)
.section(
AccordionSection::new("publish", "Publish")
.description("Where the signed bundle goes")
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"The bundle is published to the workspace channel.",
)),
),
)
.into_any_element()
}
pub(super) fn breadcrumb(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(
Breadcrumb::new("scene.breadcrumb.short")
.crumbs([
Crumb::new("workspace", "Workspace"),
Crumb::new("runs", "Runs"),
Crumb::new("run-4821", "Indexing"),
])
.on_select(|_, _, _| {}),
)
.child(
Breadcrumb::new("scene.breadcrumb.long")
.crumbs([
Crumb::new("workspace", "Workspace"),
Crumb::new("projects", "Projects"),
Crumb::new("gpui-kit", "gpui-kit"),
Crumb::new("runs", "Runs"),
Crumb::new("run-4821", "Indexing"),
])
.max_visible(3)
.on_select(|_, _, _| {})
.on_reveal(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn navigation_sections() -> Vec<SidebarSection> {
vec![
SidebarSection::new("work").title("Work").items([
SidebarItem::new("runs", "Runs")
.icon(Icon::List)
.badge("12")
.children([
SidebarItem::new("runs.active", "Active").icon(Icon::Refresh),
SidebarItem::new("runs.archived", "Archived").icon(Icon::Archive),
]),
SidebarItem::new("files", "Files").icon(Icon::Folder),
]),
SidebarSection::new("admin").title("Administration").items([
SidebarItem::new("settings", "Settings").icon(Icon::Settings),
SidebarItem::new("policy", "Managed by policy")
.icon(Icon::Key)
.disabled(true),
]),
]
}
#[derive(Default)]
struct SceneSidebar {
active: Option<SharedString>,
collapsed: bool,
}
impl Global for SceneSidebar {}
pub(super) fn sidebar(_window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneSidebar>() {
cx.set_global(SceneSidebar::default());
}
let theme = cx.theme().clone();
let active = cx
.global::<SceneSidebar>()
.active
.clone()
.unwrap_or_else(|| "runs.active".into());
let collapsed = cx.global::<SceneSidebar>().collapsed;
let rail = |ident: &'static str, collapsed: bool| {
Sidebar::new(ident)
.sections(navigation_sections())
.active(active.clone())
.collapsed(collapsed)
.footer(
crate::foundation::text(
&theme,
TypeScale::Caption,
if collapsed {
SharedString::new_static("v0")
} else {
SharedString::new_static("Fixture workspace")
},
)
.text_tone(&theme, TextTone::Faint),
)
.on_select(|id, window, cx| {
cx.global_mut::<SceneSidebar>().active = Some(id);
window.refresh();
})
};
stack(&theme)
.child(caption(
&theme,
"Fixture navigation — select a destination, toggle a branch, or collapse the rail",
))
.child(
div()
.flex()
.flex_row()
.h(px(400.0))
.gap(px(theme.space(Space::Lg)))
.child(
rail("scene.sidebar.expanded", collapsed)
.width(260.0)
.header(caption(
&theme,
if collapsed { "W" } else { "Fixture workspace" },
))
.on_collapse(|collapsed, window, cx| {
cx.global_mut::<SceneSidebar>().collapsed = collapsed;
window.refresh();
}),
)
.child(rail("scene.sidebar.collapsed", true)),
)
.child(caption(
&theme,
"Current destination is caller-owned; arrow keys move focus without navigating",
))
.into_any_element()
}
pub(super) fn sidebar_overflow(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.child(caption(&theme, "Fixture stress states — fixed slots, scrolling, long labels, and a closed current branch"))
.child(div().flex().gap(px(theme.space(Space::Lg))).h(px(320.0))
.child(div().w(px(300.0)).h_full().child(Sidebar::new("scene.sidebar-overflow.scroll")
.fill_width()
.header(caption(&theme, "Fixed header"))
.footer(caption(&theme, "Fixed footer"))
.section(SidebarSection::new("destinations").title("Destinations").items(
(0..24).map(|index| SidebarItem::new(format!("destination-{index}"),
if index == 1 { "A long destination label that needs an ellipsis".to_owned() }
else { format!("Fixture destination {}", index + 1) }).disabled(index == 2))
))
.active("destination-1")
.on_select(|_, _, _| {})))
.child(Sidebar::new("scene.sidebar-overflow.closed")
.sections(navigation_sections()).expanded_ids(&[])
.active("runs.active")
.header(caption(&theme, "Host refuses branch expansion"))
.footer(caption(&theme, "Current: Active"))
.on_select(|_, _, _| {}).on_toggle(|_, _, _, _| {})))
.into_any_element()
}
/// The page-size control of the pagination scene, kept across frames.
pub(super) struct ScenePagination {
page_size: Entity<Select>,
}
impl Global for ScenePagination {}
pub(super) fn pagination(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<ScenePagination>() {
let page_size = cx.new(|cx| {
Select::new("scene.pagination.size", window, cx)
.name("Rows per page")
.options([
SelectOption::new("25", "25 per page"),
SelectOption::new("50", "50 per page"),
SelectOption::new("100", "100 per page"),
])
.selected("50")
});
cx.set_global(ScenePagination { page_size });
}
let page_size = cx.global::<ScenePagination>().page_size.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(620.0))
.child(caption(
&theme,
"a known total, with the page size the host offered",
))
.child(
Pagination::new("scene.pagination.known")
.page(9)
.total_pages(20)
.page_size(page_size)
.on_select(|_, _, _| {}),
)
// A host that only knows there is another page says exactly that: no
// last-page control, no numbers, and no total.
.child(caption(
&theme,
"an unknown total: no numbers, no last page, and no count invented",
))
.child(
Pagination::new("scene.pagination.unknown")
.page(3)
.unknown_total(true)
.on_select(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn wizard(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(720.0))
.child(caption(
&theme,
"horizontal, with a blocked step and a failed one",
))
.child(
Wizard::new("scene.wizard.release")
.steps([
WizardStep::new("prepare", "Prepare")
.description("Check the workspace is clean")
.complete(),
WizardStep::new("build", "Build")
.description("Compile every target")
.failed("The build failed on the test target."),
WizardStep::new("sign", "Sign").current(),
WizardStep::new("publish", "Publish")
.blocked("Approval is required for this workspace."),
])
.body(crate::foundation::text(
&theme,
TypeScale::Body,
SharedString::new_static("The body of the current step belongs to the caller."),
))
.back_to("build")
.on_navigate(|_, _, _| {}),
)
.child(caption(&theme, "vertical, finishing"))
.child(
Wizard::new("scene.wizard.setup")
.vertical()
.steps([
WizardStep::new("account", "Account").complete(),
WizardStep::new("workspace", "Workspace").complete(),
WizardStep::new("review", "Review").current(),
])
.back_to("workspace")
.finish(true)
.on_navigate(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn undo_history(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(caption(
&theme,
"the caller owns every revision and whether it can be restored",
))
.child(
UndoHistory::new("scene.undo-history", "Document undo history")
.entries([
HistoryEntry::new("opened", "Opened the fixture")
.description("The initial verified document")
.source("Fixture host")
.time("10:14"),
HistoryEntry::new("renamed", "Renamed the title")
.source("Alex")
.time("10:16"),
HistoryEntry::new("imported", "Imported archived blocks")
.description("The archive is no longer available")
.source("Archive")
.time("10:18")
.unavailable("This revision cannot be restored without the archive."),
HistoryEntry::new("current", "Reordered the summary")
.description("Current document")
.source("Alex")
.time("10:21"),
HistoryEntry::new("draft", "Drafted a new conclusion")
.source("Fixture host")
.time("10:23"),
])
.current("current")
.on_jump(|_, _, _| {}),
)
.into_any_element()
}
pub(super) fn collapsible(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(
Collapsible::new("scene.collapsible.open", "Advanced")
.description("Settings most runs never touch")
.open(true)
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"Requests go out over the system proxy.",
))
.on_toggle(|_, _, _| {}),
)
.child(
Collapsible::new("scene.collapsible.shut", "Diagnostics")
.description("Nothing is collected until this is opened")
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"This body is absent from the tree while it is shut.",
))
.on_toggle(|_, _, _| {}),
)
.child(
Collapsible::new("scene.collapsible.refused", "Managed by policy")
.description("This machine cannot change these")
.disabled(true)
.body(crate::foundation::text(
&theme,
TypeScale::Body,
"Set by the administrator.",
)),
)
.into_any_element()
}
//! Surfaces that appear above the page and take a decision.
use super::support::*;
pub(super) fn kbd(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
// A keystroke is only ever read beside the thing it performs, so the
// exhibit shows it where it is used: at the end of a menu row, at the end
// of a sentence, and in the run of special keys whose glyphs are the part
// that goes wrong.
let named = |label: &'static str, keystroke: &'static str, id: &'static str| {
div()
.row()
.w_full()
.items_center()
.justify_between()
.gap_token(&theme, Space::Md)
.py_token(&theme, Space::Xs)
.child(crate::foundation::text(&theme, TypeScale::Body, label))
.child(Kbd::new(keystroke).id(id))
};
stack(&theme)
.w(px(420.0))
.child(caption(&theme, "the shortcut sits with the thing it does"))
.child(
div()
.column()
.w_full()
.card_surface(&theme, CardVariant::Elevated)
.px_token(&theme, Space::Md)
.py_token(&theme, Space::Sm)
.child(named(
"Open the command palette",
"cmd-shift-p",
"scene.kbd.palette",
))
.child(div().w_full().h(px(theme.space(Space::Sm))))
.child(named("Copy the selection", "ctrl-c", "scene.kbd.copy"))
.child(div().w_full().h(px(theme.space(Space::Sm))))
.child(named("Rename in place", "cmd-alt-r", "scene.kbd.rename")),
)
.child(caption(
&theme,
"the keys that need the bundled symbol face",
))
.child(
row(&theme)
.gap_token(&theme, Space::Sm)
.child(Kbd::new("enter").id("scene.kbd.confirm"))
.child(Kbd::new("escape").id("scene.kbd.dismiss"))
.child(Kbd::new("backspace").id("scene.kbd.erase"))
.child(Kbd::new("delete").id("scene.kbd.delete"))
.child(Kbd::new("tab").id("scene.kbd.advance"))
.child(Kbd::new("space").id("scene.kbd.space"))
.child(Kbd::new("up").id("scene.kbd.up"))
.child(Kbd::new("down").id("scene.kbd.down")),
)
.child(
div()
.row()
.items_center()
.gap_token(&theme, Space::Xs)
.child(crate::foundation::text(&theme, TypeScale::Body, "Press"))
.child(Kbd::new("cmd-enter").id("scene.kbd.inline"))
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"to send the message.",
))
.text_color(theme.colors.text_muted),
)
.into_any_element()
}
/// A page for a modal layer to sit over.
///
/// A scrim drawn over an empty canvas is indistinguishable from a fill: the
/// only thing that shows it is translucent is what stays legible underneath
/// it. So every scene in this file that raises a modal layer puts a page worth
/// obscuring behind it rather than one line of text.
fn page_behind(theme: &Theme, title: &'static str) -> gpui::Div {
let card = |heading: &'static str, lines: usize| {
div()
.flex_1()
.min_w_0()
.card_surface(theme, CardVariant::Elevated)
.overflow_hidden()
.child(filler(theme, heading, lines))
};
div()
.column()
.w_full()
.gap(px(theme.space(Space::Md)))
.child(crate::foundation::text(theme, TypeScale::Subtitle, title))
.child(
div()
.row()
.w_full()
.gap(px(theme.space(Space::Md)))
.child(card("Runs", 5))
.child(card("Details", 5)),
)
}
pub(super) fn overlay(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.h(px(320.0))
.child(page_behind(&theme, "Workspace settings"))
.child(
Overlay::modal("scene.overlay.dialog")
.placement(Placement::Center)
.child(
crate::overlay::surface(
"scene.overlay.surface",
&theme,
crate::overlay::OverlaySurface::MODAL,
)
.w(px(320.0))
.p(px(theme.spacing.lg))
.gap(px(theme.spacing.sm))
.child(crate::foundation::text(
&theme,
TypeScale::Subtitle,
"Delete this workspace?",
))
.child(
crate::foundation::text(
&theme,
TypeScale::Body,
"Its runs, filters and saved views are removed for everyone. This \
cannot be undone.",
)
.text_tone(&theme, TextTone::Muted),
)
.child(
div()
.row()
.justify_end()
.gap(px(theme.spacing.sm))
.child(
Button::new("scene.overlay.cancel")
.label("Cancel")
.secondary()
.on_click(|_, _| {}),
)
.child(
Button::new("scene.overlay.confirm")
.label("Delete")
.danger()
.on_click(|_, _| {}),
),
),
),
)
.into_any_element()
}
/// The dialog the scene shows, kept across frames.
///
/// A dialog owns whether it is open and which element had the keyboard before
/// it opened, so rebuilding it every frame would reopen it every frame.
pub(super) struct SceneDialog {
replace: Entity<Dialog>,
}
impl Global for SceneDialog {}
pub(super) fn dialog(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneDialog>() {
let replace = cx.new(|cx| {
Dialog::new("scene.dialog.replace", window, cx)
.title("Replace the existing theme?")
.description(
"The application owns this decision. The dialog presents it and reports what \
was chosen.",
)
.cancel_label("Cancel")
.confirm_label("Replace")
});
replace.update(cx, |dialog, cx| dialog.open(window, cx));
cx.set_global(SceneDialog { replace });
}
let replace = cx.global::<SceneDialog>().replace.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(560.0))
.h(px(360.0))
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Content behind the dialog",
))
.child(replace)
.into_any_element()
}
pub(super) fn tooltip(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.child(
row(&theme).child(
div()
.id("scene.tooltip.host")
.tip("scene.tooltip.export", "Writes the theme to a file on disk")
.child(
Button::new("scene.tooltip.export")
.label("Export theme")
.accessible_description("Writes the theme to a file on disk")
.secondary()
.on_click(|_, _| {}),
),
),
)
// Hover help only exists while a pointer rests on the control, so the
// surface itself is also shown outright, where it can be reviewed.
.child(
row(&theme).child(
Tooltip::new("scene.tooltip.help", "Writes the theme to a file on disk")
.describes("scene.tooltip.export"),
),
)
.into_any_element()
}
/// The menu family the scenes show, kept across frames.
///
/// Each of these owns whether it is open, where the keyboard is, and which
/// submenu stands expanded, so rebuilding them every frame would reopen them
/// every frame. Building them once is also what makes the capture static.
pub(super) struct SceneMenus {
menu: Entity<Menu>,
context: Entity<ContextMenu>,
palette: Entity<CommandPalette>,
popover: Entity<Popover>,
}
impl Global for SceneMenus {}
pub(super) fn menu_items() -> Vec<MenuItem> {
vec![
MenuItem::section("group", "This run"),
MenuItem::command("copy", "Copy run id")
.icon(Icon::Copy)
.shortcut("cmd-c"),
MenuItem::check("follow", "Follow output", true),
MenuItem::separator("rule"),
MenuItem::command("publish", "Publish").disabled(true),
MenuItem::submenu(
"share",
"Share",
[
MenuItem::command("share.link", "Copy link").shortcut("cmd-shift-c"),
MenuItem::command("share.export", "Export as file"),
],
),
MenuItem::separator("destroy"),
MenuItem::command("delete", "Delete this run")
.icon(Icon::Trash)
.destructive(true),
]
}
pub(super) fn scene_commands() -> Vec<Command> {
vec![
Command::new("workspace.open", "Open workspace")
.section("Workspace")
.shortcut("cmd-o"),
Command::new("workspace.close", "Close workspace").section("Workspace"),
Command::new("workspace.publish", "Publish workspace")
.section("Workspace")
.unavailable("Approval is required"),
Command::new("editor.wrap", "Toggle word wrap").section("Editor"),
]
}
pub(super) fn ensure_menus(window: &mut Window, cx: &mut App) {
if cx.has_global::<SceneMenus>() {
return;
}
let menu = cx.new(|cx| {
Menu::new("scene.menu.run", window, cx)
.trigger("Run actions")
.items(menu_items())
});
menu.update(cx, |menu, cx| {
menu.open_submenu("share", window, cx);
});
// The catalog reviews the drawn surface. Left on `Native`, `open_at` would
// hand this list to the operating system on macOS and Windows: a modal
// popup no capture can hold, opened under every scene that shares these
// fixtures, and on Windows it takes the keyboard from the `menu` scene's
// exhibit while the native UIA check reads it.
let context = cx.new(|cx| {
ContextMenu::new("scene.context.run", window, cx)
.name("Run actions")
.target("run-a04")
.presentation(ContextMenuPresentation::InWindow)
.menu(menu_items())
.content(|_, cx| {
let theme = cx.theme().clone();
div()
.w(px(320.0))
.p(px(theme.spacing.md))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Right-click this fixture row",
))
.into_any_element()
})
});
context.update(cx, |context, cx| {
context
.open_at(gpui::point(px(180.0), px(150.0)), window, cx)
.expect("in-window fixture");
});
let palette = cx.new(|cx| {
CommandPalette::new("scene.palette.commands", window, cx).commands(scene_commands())
});
palette.update(cx, |palette, cx| palette.set_query("work", cx));
let popover = cx.new(|cx| {
Popover::new("scene.popover.filters", window, cx)
.trigger("Filters")
.content(|_, cx| {
let theme = cx.theme().clone();
div()
.column()
.w(px(260.0))
.gap(px(theme.spacing.sm))
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Anything can live in a popover.",
))
.child(
Checkbox::new("scene.popover.failing")
.label("Failing runs only")
.on_change(|_, _, _| {}),
)
.into_any_element()
})
});
popover.update(cx, |popover, cx| popover.open(window, cx));
cx.set_global(SceneMenus {
menu,
context,
palette,
popover,
});
}
pub(super) fn popover(window: &mut Window, cx: &mut App) -> AnyElement {
ensure_menus(window, cx);
let popover = cx.global::<SceneMenus>().popover.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.h(px(320.0))
// The trigger keeps its place while the surface is open, because the
// surface is anchored to it rather than laid out beside it.
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"The trigger owns whether the surface is open.",
))
.child(popover)
.into_any_element()
}
pub(super) fn menu(window: &mut Window, cx: &mut App) -> AnyElement {
ensure_menus(window, cx);
let menus = cx.global::<SceneMenus>();
let menu = menus.menu.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(560.0))
.h(px(320.0))
// An open surface is an overlay and takes no room in the flow, so the
// scene reserves the panel's review area explicitly.
.child(div().h(px(300.0)).child(menu))
.into_any_element()
}
pub(super) fn context_menu(window: &mut Window, cx: &mut App) -> AnyElement {
ensure_menus(window, cx);
let context = cx.global::<SceneMenus>().context.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(560.0))
.h(px(400.0))
// Opening a context menu reports the row that was pointed at; what is
// selected stays the host's answer.
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"The right-click reports the row. Nothing is selected by it.",
))
.child(context)
.into_any_element()
}
pub(super) fn command_palette(window: &mut Window, cx: &mut App) -> AnyElement {
ensure_menus(window, cx);
let palette = cx.global::<SceneMenus>().palette.clone();
let theme = cx.theme().clone();
// A palette is summoned over whatever is on screen, so it sits centred
// near the top of the surface rather than in a corner of it.
stack(&theme)
.w_full()
.h(px(420.0))
.items_center()
.pt(px(theme.spacing.xxl))
.child(palette)
.into_any_element()
}
/// The notification layer the scene shows, kept across frames.
///
/// The stack, each timer, and each entry animation outlive a frame, so the
/// layer is built once and the toasts are pushed once with it.
pub(super) struct SceneToasts {
layer: Entity<ToastLayer>,
}
impl Global for SceneToasts {}
pub(super) fn toast(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneToasts>() {
let layer = cx.new(|cx| ToastLayer::new(window, cx).capacity(4));
cx.set_global(SceneToasts { layer });
toast_push(
window,
cx,
Toast::new("scene.toast.saved", "Theme exported to disk").tone(Tone::Success),
);
toast_push(
window,
cx,
Toast::new("scene.toast.stale", "Refreshing the model catalog failed")
.tone(Tone::Warning)
.detail("The last verified catalog is still shown."),
);
toast_push(
window,
cx,
// A refusal, so the offer is the thing that could change the
// answer. Retrying an unapproved call only gets refused again.
Toast::new(
"scene.toast.refused",
"The host refused to publish this run",
)
.tone(Tone::Warning)
.detail("Approval is required for this workspace.")
.action("Request approval", |_, _| {}),
);
// The failure beside the refusal, so the two tones are on screen
// together and the difference between them is a picture rather than a
// claim.
toast_push(
window,
cx,
Toast::new("scene.toast.failed", "Publishing this run failed")
.tone(Tone::Danger)
.detail("The publish service did not respond.")
.action("Try again", |_, _| {}),
);
}
let layer = cx.global::<SceneToasts>().layer.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(560.0))
.h(px(360.0))
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"Content behind the notifications",
))
// A failure keeps its report on screen; only the success times out.
.child(crate::foundation::text(
&theme,
TypeScale::Body,
"A danger or warning toast stays until it is dismissed.",
))
.child(layer)
.into_any_element()
}
/// The notification centre the scene shows, kept across frames.
pub(super) struct SceneNotifications {
centre: Entity<NotificationCenter>,
}
impl Global for SceneNotifications {}
pub(super) fn notification_center(_window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneNotifications>() {
let centre = cx.new(|cx| NotificationCenter::new("scene.notifications", cx));
centre.update(cx, |centre, cx| {
centre.record(
Notification::new("scene.notify.exported", "Theme exported to disk")
.tone(Tone::Success)
.at("9:41")
.read(true),
cx,
);
centre.record(
Notification::new("scene.notify.stale", "Refreshing the model catalog failed")
.tone(Tone::Warning)
.detail("The last verified catalog is still shown.")
.at("9:44"),
cx,
);
centre.record(
Notification::new(
"scene.notify.refused",
"The host refused to publish this run",
)
.tone(Tone::Warning)
.detail("Approval is required for this workspace.")
.at("9:46")
.action("Request approval", |_, _| {}),
cx,
);
});
cx.set_global(SceneNotifications { centre });
}
let centre = cx.global::<SceneNotifications>().centre.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
// The same three reports the toast scene shows, after their toasts
// have gone.
.child(caption(
&theme,
"what the toasts said, still here once they timed out",
))
.child(centre)
.into_any_element()
}
/// Glass over the page it covers, which is the only way to see that the
/// backdrop is blurred rather than merely tinted.
pub(super) fn frost(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let card = |title: &'static str, body: &'static str| {
div()
.column()
.gap(px(theme.space(Space::Xs)))
.p(px(theme.space(Space::Md)))
.child(crate::foundation::text(
&theme,
TypeScale::Label,
SharedString::from(title),
))
.child(caption(&theme, body))
};
// What is behind the glass is a page, not a test pattern. A block of
// accent stripes dropped into a panel reads as a hole cut in it, and it
// answers the wrong question anyway: the thing a reader has to be able to
// judge is whether text they can otherwise read has gone out of focus.
stack(&theme)
.w(px(480.0))
.child(caption(&theme, "A floating surface on glass"))
.child(
div()
.relative()
.h(px(200.0))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(filler(&theme, "Document", 8))
.child(
div()
.absolute()
.top(px(48.0))
// Far enough left to cross the lines. Placed clear of
// them the glass had nothing behind it, so the picture
// could not answer the one question it is here for:
// whether text a reader can otherwise read has gone
// out of focus.
.left(px(64.0))
.w(px(240.0))
.child(
Frost::new("scene.frost.popover")
.radius(Radius::Card)
.child(card(
"Rename",
"The page behind stays visible, out of focus",
)),
),
),
)
.child(caption(
&theme,
"The same glass, blurred further and raised above the panel",
))
.child(
div()
.relative()
.h(px(160.0))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(filler(&theme, "Files", 6))
.child(
div()
.absolute()
.top(px(32.0))
.left(px(64.0))
.w(px(280.0))
.child(
Frost::new("scene.frost.rail")
// A surface a step above the page it covers.
// Tinted with the page's own colour the glass
// had no surface of its own: at any alpha, a
// fill the colour of what is behind it adds
// nothing, and the rail's words landed
// directly on the words of the file list.
.surface(gpui_kit_theme::Surface::Raised)
.radius(Radius::Dialog)
.blur(32.0)
.child(card(
"Rail",
"The lines behind stay legible and out of focus",
)),
),
),
)
.into_any_element()
}
/// The optics, one at a time and then together, over a backdrop with enough
/// structure in it that a bend is visible as a bend rather than as a smudge.
///
/// The presets sit side by side deliberately: `Frosted` is the control, and
/// what separates it from `Lens` is exactly the refraction, so a reviewer
/// looking at the pair is looking at the thing that changed.
/// One square of the backdrop every optics plate is read against, and the two
/// plate footprints built from it. Both are whole numbers of squares.
const TILE: f32 = 48.0;
const PLATE_WIDTH: f32 = TILE * 8.0;
const PLATE_HEIGHT: f32 = TILE * 3.0;
const JOIN_WIDTH: f32 = PLATE_WIDTH;
/// A deterministic raster media fixture, not downloaded product content.
fn glass_media() -> Arc<RenderImage> {
static IMAGE: OnceLock<Arc<RenderImage>> = OnceLock::new();
IMAGE
.get_or_init(|| {
let mut pixels = Vec::with_capacity(480 * 144 * 4);
for y in 0..144 {
for x in 0..480 {
let ridge = 65 + ((x as f32 * 0.018).sin() * 30.0) as i32;
let rgba = if y > ridge {
[28, 91 + (x / 8) as u8, 91, 255]
} else {
[110 + (y / 2) as u8, 165, 220, 255]
};
pixels.extend_from_slice(&rgba);
}
}
Arc::new(
RenderImage::from_rgba(size(DevicePixels(480), DevicePixels(144)), pixels)
.expect("media fixture dimensions"),
)
})
.clone()
}
/// A constrained media tile owns size; its caption only names edge anchors.
pub(super) fn media_caption(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let tile = |id: &'static str, reduced: bool| {
let material_theme = theme.clone().with_reduce_transparency(reduced);
div()
.relative()
.w_full()
.h(px(220.0))
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
gpui::img(glass_media())
.size_full()
.radius(&theme, Radius::Card)
.object_fit(gpui::ObjectFit::Cover),
)
.child(
crate::overlay::surface(
id,
&material_theme,
crate::overlay::OverlaySurface::MEDIA_CAPTION,
)
.absolute()
.left_0()
.right_0()
.bottom_0()
.flex_row()
.items_center()
.gap_token(&theme, Space::Md)
.p_token(&theme, Space::Md)
.child(
div()
.flex_1()
.min_w_0()
.type_scale(&theme, TypeScale::Label)
.child(if reduced {
"Media fixture · reduced transparency"
} else {
"Media fixture · Clear caption"
}),
)
.child(
div()
.type_scale(&theme, TypeScale::Caption)
.child("3 projects"),
)
.child(
Button::new(format!("{id}.new"))
.label("New project")
.ghost()
.on_click(|_, _| {}),
),
)
};
stack(&theme)
.w_full()
.child(caption(
&theme,
"Parent-constrained width; caption height follows its content",
))
.child(tile("scene.media-caption.clear", false))
.child(tile("scene.media-caption.reduced", true))
.into_any_element()
}
pub(super) fn glass(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let label = |title: &'static str, body: &'static str| {
div()
.column()
.gap(px(theme.space(Space::Xs)))
.p(px(theme.space(Space::Md)))
.child(div().type_scale(&theme, TypeScale::Label).child(title))
.child(div().type_scale(&theme, TypeScale::Caption).child(body))
};
// A ruled checkerboard makes the material contract visible: Regular's
// rim refracts the scattered source, bending colour and luminance bands
// without restoring recognizable fine lines. Clear stays sharp because
// its blur is zero. The board is neutral, and every plate is an exact
// number of squares across and down: a board cut through the middle of a
// square at the plate edge reads as a broken pattern rather than as the
// ruled backdrop the optics are being measured against.
let checker_light = theme.colors.text_faint;
let checker_dark = theme.colors.canvas;
let checker_rule = theme.colors.divider.opacity(0.55);
let checkerboard =
|width: f32, height: f32| {
let columns = (width / TILE).ceil() as usize;
let rows = (height / TILE).ceil() as usize;
div()
.absolute()
.top(px(0.0))
.left(px(0.0))
.w(px(width))
.h(px(height))
.column()
.overflow_hidden()
.children((0..rows).map(|row| {
div()
.flex()
.flex_none()
.h(px(TILE))
.children((0..columns).map(move |column| {
div().flex_none().w(px(TILE)).h(px(TILE)).bg(
if (row + column) % 2 == 0 {
checker_light
} else {
checker_dark
},
)
}))
}))
.children((0..=(width / 12.0) as usize).map(|column| {
div()
.absolute()
.top(px(0.0))
.left(px(column as f32 * 12.0))
.w(px(1.0))
.h(px(height))
.bg(checker_rule)
}))
.children((0..=(height / 12.0) as usize).map(|row| {
div()
.absolute()
.top(px(row as f32 * 12.0))
.left(px(0.0))
.w(px(width))
.h(px(1.0))
.bg(checker_rule)
}))
};
let plate =
|ident: &'static str, preset: GlassPreset, title: &'static str, body: &'static str| {
div()
.relative()
.h(px(PLATE_HEIGHT))
.w(px(PLATE_WIDTH))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
if preset == GlassPreset::Clear || ident == "scene.glass.media" {
gpui::img(glass_media())
.object_fit(gpui::ObjectFit::Cover)
.w(px(PLATE_WIDTH))
.h(px(PLATE_HEIGHT))
.into_any_element()
} else {
checkerboard(PLATE_WIDTH, PLATE_HEIGHT).into_any_element()
},
)
.when(ident == "scene.glass.liquid", |element| {
element.child(div().absolute().inset_0().child(filler(
&theme,
"Document text behind Regular glass",
6,
)))
})
.child(
div()
.absolute()
.top(px(28.0))
.left(px(52.0))
.w(px(280.0))
.child(
Glass::new(ident)
.preset(preset)
.dimmed(preset == GlassPreset::Clear)
.adaptive(true)
.radius(Radius::Dialog)
.child(label(title, body)),
),
)
};
stack(&theme)
.w(px(900.0))
.child(caption(
&theme,
"Clear: dark Frosted / reduced transparency Clear + 35% dimming / media only",
))
.child(
row(&theme)
.child(crate::foundation::ThemeOverlay::theme(
theme.clone().with_reduce_transparency(true),
plate(
"scene.glass.frosted",
GlassPreset::Clear,
"Clear: reduced transparency",
"Dark Frosted and light content in every theme",
),
))
.child(plate(
"scene.glass.clear",
GlassPreset::Clear,
"Clear + dimmed",
"A sharp media fixture, never default chrome",
)),
)
.child(caption(
&theme,
"Regular over text and media: blur, achromatic wash, lensing and hairline",
))
.child(
row(&theme)
.child(plate(
"scene.glass.liquid",
GlassPreset::Liquid,
"Regular Liquid",
"Large reading surfaces keep the window appearance",
))
.child(plate(
"scene.glass.media",
GlassPreset::Liquid,
"Regular on media",
"The same material, not the Clear variant",
)),
)
.child(caption(
&theme,
"Compact Regular controls: appearance may flip; no opaque face",
))
.child(
div()
.relative()
.h(px(TILE * 2.0))
.w(px(PLATE_WIDTH))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(div().absolute().inset_0().bg(gpui::white()))
.child(
div()
.absolute()
.right_0()
.top_0()
.w(px(PLATE_WIDTH / 2.0))
.h_full()
.bg(gpui::black()),
)
.child(
div()
.absolute()
.inset_0()
.flex()
.items_center()
.justify_center()
.gap(px(theme.space(Space::Sm)))
.child(
Glass::new("scene.glass.regular.one")
.preset(GlassPreset::Liquid)
.surface(Surface::Raised)
.radius(Radius::Pill)
.adaptive(true)
.child(
div()
.w(px(110.0))
.h(px(32.0))
.flex()
.items_center()
.justify_center()
.child("Portal"),
),
)
.child(
Glass::new("scene.glass.regular.two")
.preset(GlassPreset::Liquid)
.radius(Radius::Pill)
.adaptive(true)
.child(
div()
.w(px(110.0))
.h(px(32.0))
.rounded(px(theme.radius(Radius::Pill)))
.bg(theme.colors.selected)
.flex()
.items_center()
.justify_center()
.child("Harbour"),
),
),
),
)
// The last two demonstrations sit side by side so the whole scene
// stays inside the window a real display can give the gallery, which
// is where the DirectX renderer gets looked at.
.child(
div()
.flex()
.flex_row()
.gap(px(theme.space(Space::Md)))
.child(
div()
.column()
.gap(px(theme.space(Space::Sm)))
.child(caption(&theme, "Fused: two panes joined into one body"))
.child(
div()
.relative()
.h(px(PLATE_HEIGHT))
.w(px(JOIN_WIDTH))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(checkerboard(JOIN_WIDTH, PLATE_HEIGHT))
.child(
div().absolute().top(px(40.0)).left(px(40.0)).child(
GlassGroup::new("scene.glass.fused")
.preset(GlassPreset::Liquid)
.radius(Radius::Dialog)
.gap(12.0)
.pane(
"scene.glass.fused.left",
label("Left", "One lobe of the body"),
)
.pane(
"scene.glass.fused.right",
label("Right", "Joined across the gap"),
),
),
),
),
)
.child(
div()
.column()
.gap(px(theme.space(Space::Sm)))
.child(caption(&theme, "Large adaptive surfaces never flip"))
.child(
div()
.relative()
.h(px(PLATE_HEIGHT))
.w(px(JOIN_WIDTH))
.surface(&theme, Surface::Panel)
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
div()
.absolute()
.top(px(0.0))
.left(px(0.0))
.w(px(JOIN_WIDTH / 2.0))
.h(px(PLATE_HEIGHT))
.bg(gpui::white()),
)
.child(
div()
.absolute()
.top(px(0.0))
.left(px(JOIN_WIDTH / 2.0))
.w(px(JOIN_WIDTH / 2.0))
.h(px(PLATE_HEIGHT))
.bg(gpui::black()),
)
.child(
div()
.absolute()
.top(px(28.0))
.left(px(15.0))
.w(px(160.0))
.child(
Glass::new("scene.glass.adaptive.bright")
.preset(GlassPreset::Liquid)
.radius(Radius::Dialog)
.adaptive(true)
.adaptive_appearance(true)
.child(label(
"Bright",
"The reading lands next frame",
)),
),
)
.child(
div()
.absolute()
.top(px(28.0))
.left(px(JOIN_WIDTH / 2.0 + 15.0))
.w(px(160.0))
.child(
Glass::new("scene.glass.adaptive.dark")
.preset(GlassPreset::Liquid)
.radius(Radius::Dialog)
.adaptive(true)
.child(label("Dark", "The same glass, other side")),
),
),
),
),
)
.child(caption(
&theme,
"Focused: one inward report edge — Regular, Clear + dimmed, Frosted, reduced",
))
.child(
row(&theme).children(
[
("regular", GlassPreset::Liquid, false, "Regular"),
("clear", GlassPreset::Clear, false, "Clear + dimmed"),
("frosted", GlassPreset::Frosted, false, "Frosted"),
("reduced", GlassPreset::Clear, true, "Reduced"),
]
.into_iter()
.map(|(name, preset, reduced, title)| {
div()
.relative()
.w(px(210.0))
.h(px(96.0))
.radius(&theme, Radius::Card)
.overflow_hidden()
.child(
gpui::img(glass_media())
.size_full()
.object_fit(gpui::ObjectFit::Cover),
)
.child(
div()
.absolute()
.left(px(12.0))
.right(px(12.0))
.top(px(24.0))
.child(crate::foundation::ThemeOverlay::theme(
theme.clone().with_reduce_transparency(reduced),
Glass::new(format!("scene.glass.focused.{name}"))
.preset(preset)
.dimmed(preset == GlassPreset::Clear)
.focused(true)
.radius(Radius::Pill)
.child(
div()
.w_full()
.h(px(40.0))
.flex()
.items_center()
.justify_center()
.type_scale(&theme, TypeScale::Label)
.child(title),
),
)),
)
}),
),
)
.child(caption(
&theme,
"Budget fallback: opaque cards, never holes — R keeps its focus report",
))
.child(div().flex().gap(px(4.0)).children(('A'..='R').map(|name| {
Glass::new(format!("scene.glass.budget.{name}"))
.focused(name == 'R')
.child(
div()
.w(px(38.0))
.h(px(30.0))
.flex()
.items_center()
.justify_center()
.child(name.to_string()),
)
})))
.child(caption(
&theme,
"Nine lobes exceed the fused budget: every pane retains an opaque face",
))
.child(
('A'..='I').fold(GlassGroup::new("scene.glass.lobe-budget"), |group, name| {
group.pane(
format!("scene.glass.lobe-budget.{name}"),
div()
.w(px(58.0))
.h(px(30.0))
.flex()
.items_center()
.justify_center()
.child(name.to_string()),
)
}),
)
.into_any_element()
}
/// Optical parameters over a deterministic ruled fixture, without a wash or
/// decorative highlight hiding the displacement. Labels sit outside the lens.
pub(super) fn glass_optics(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let specimens = [
("identity", "Index 1 / unchanged", 1., 24., 24., 0., 0.),
("water", "Index 1.33 / shallow bend", 1.33, 24., 24., 0., 0.),
("glass", "Index 1.5 / deeper bend", 1.5, 24., 24., 0., 0.),
(
"thick",
"48px thickness / same index",
1.5,
48.,
24.,
0.,
0.,
),
("contact", "Source plane at base", 1.5, 24., 0., 0., 0.),
(
"dispersion",
"Spectral indices / no highlight",
1.5,
24.,
24.,
0.25,
0.,
),
(
"reflection",
"Fresnel / same surface normal",
1.5,
24.,
24.,
0.,
1.,
),
(
"scatter",
"Same optics / 8px scattering",
1.5,
24.,
24.,
0.,
0.,
),
];
stack(&theme)
.w(px(900.))
.child(caption(
&theme,
"Snell refraction — fixture grid; foreground content is not distorted",
))
.children(specimens.chunks(4).map(|specimens| {
row(&theme).children(specimens.iter().map(
|&(name, title, index, thickness, depth, dispersion, specular)| {
div()
.column()
.gap_token(&theme, Space::Sm)
.w(px(210.))
.child(caption(&theme, title))
.child(
div()
.relative()
.w(px(210.))
.h(px(180.))
.overflow_hidden()
.bg(theme.colors.canvas)
.children((0..18).map(|line| {
div()
.absolute()
.top(px(line as f32 * 10.))
.left_0()
.w_full()
.h(px(1.))
.bg(theme.colors.text_faint)
}))
.children((0..21).map(|line| {
div()
.absolute()
.left(px(line as f32 * 10.))
.top_0()
.h_full()
.w(px(1.))
.bg(theme.colors.text_faint)
}))
.child(
div()
.absolute()
.top(px(22.))
.left(px(25.))
.w(px(160.))
.child(
Glass::new(format!("scene.glass-optics.{name}"))
.preset(GlassPreset::Lens)
.radius_px(48.)
.refraction(1.)
.thickness(thickness)
.refractive_index(index)
.backdrop_depth(depth)
.dispersion(dispersion)
.specular(specular)
.blur(if name == "scatter" { 8. } else { 0. })
.track_pointer(name == "reflection")
.pressable(true)
.child(
div()
.w_full()
.h(px(136.))
.flex()
.items_center()
.justify_center()
.child(
div()
.px_token(&theme, Space::Sm)
.bg(theme.colors.panel)
.child("Aa 123"),
),
),
),
),
)
},
))
}))
.child(caption(
&theme,
"Fused Regular / zero blur — one height field; press changes thickness and its normal",
))
.child(
div()
.relative()
.w(px(864.))
.h(px(120.))
.overflow_hidden()
.children((0..87).map(|line| {
div()
.absolute()
.left(px(line as f32 * 10.))
.top_0()
.h_full()
.w(px(1.))
.bg(theme.colors.text_faint)
}))
.children((0..12).map(|line| {
div()
.absolute()
.top(px(line as f32 * 10.))
.left_0()
.w_full()
.h(px(1.))
.bg(theme.colors.text_faint)
}))
.child(
div().absolute().left(px(24.)).top(px(24.)).child(
GlassGroup::new("scene.glass-optics.fused")
.preset(GlassPreset::Liquid)
.blur(0.)
.thickness(48.)
.refractive_index(1.5)
.backdrop_depth(24.)
.radius(Radius::Pill)
.gap(8.)
.merge(32.)
.pressable(true)
.pane(
"scene.glass-optics.fused.left",
div()
.w(px(380.))
.h(px(72.))
.flex()
.items_center()
.justify_center()
.child("Left"),
)
.pane(
"scene.glass-optics.fused.right",
div()
.w(px(380.))
.h(px(72.))
.flex()
.items_center()
.justify_center()
.child("Right"),
),
),
),
)
.into_any_element()
}
/// Colour belongs to the optical material, including a group's bridge.
/// Captions remain outside the specimens so tint strength is visible.
pub(super) fn glass_materials(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let specimens = [
("protected", "Default body protection", true, None),
("material", "Caller-owned legibility", false, None),
(
"tint-partial",
"25% tint / visible backdrop",
true,
Some(0.25),
),
(
"tint-solid",
"100% tint / rim above colour",
true,
Some(1.0),
),
];
let backdrop = || {
div()
.relative()
.w(px(200.0))
.h(px(112.0))
.bg(gpui::rgb(0xffffff))
.children((0..8).map(|stripe| {
div()
.absolute()
.left(px(stripe as f32 * 25.0))
.top_0()
.w(px(12.0))
.h_full()
.bg(gpui::rgb(0x8090a0))
}))
};
stack(&theme)
.w(px(900.0))
.child(caption(
&theme,
"Material policy / deterministic stripe fixture, not native reference",
))
.child(
row(&theme).children(specimens.into_iter().map(|(id, title, protect, tint)| {
let mut glass = Glass::new(format!("scene.glass-materials.{id}"))
.radius_px(24.0)
.blur(2.0)
.protect_text_contrast(protect);
if let Some(alpha) = tint {
glass = glass.tint(theme.colors.accent.opacity(alpha));
}
div()
.column()
.gap_token(&theme, Space::Sm)
.child(caption(&theme, title))
.child(
backdrop().child(
div()
.absolute()
.left(px(20.0))
.top(px(24.0))
.child(glass.child(div().w(px(160.0)).h(px(64.0)))),
),
)
})),
)
.child(caption(
&theme,
"Tint across joined panes / reduced-transparency fallback",
))
.child(
row(&theme).children([false, true].into_iter().map(|reduced| {
let id = if reduced { "reduced" } else { "joined" };
let tint = theme.colors.accent.opacity(0.25);
let group = GlassGroup::new(format!("scene.glass-materials.{id}"))
.radius(Radius::Pill)
.gap(8.0)
.merge(32.0)
.blur(2.0)
.tint(tint)
.pane(
format!("scene.glass-materials.{id}.left"),
div().w(px(68.0)).h(px(56.0)),
)
.pane(
format!("scene.glass-materials.{id}.right"),
div().w(px(68.0)).h(px(56.0)),
);
backdrop().child(div().absolute().left(px(28.0)).top(px(28.0)).child(
ThemeOverlay::new(move |t| t.clone().with_reduce_transparency(reduced), group),
))
})),
)
.child(caption(
&theme,
"Rounded foreground clips / surface shadows and joined optics stay outside",
))
.child(
row(&theme)
.child(
backdrop().child(
div().absolute().left(px(20.0)).top(px(24.0)).child(
Glass::new("scene.glass-materials.clip-surface")
.radius_px(24.0)
.child(div().w(px(160.0)).h(px(64.0)).bg(theme.colors.accent)),
),
),
)
.child(
backdrop().child(
div().absolute().left(px(28.0)).top(px(28.0)).child(
GlassGroup::new("scene.glass-materials.clip-group")
.radius(Radius::Pill)
.gap(8.0)
.merge(32.0)
.pane(
"scene.glass-materials.clip-left",
div().w(px(68.0)).h(px(56.0)).bg(theme.colors.accent),
)
.pane(
"scene.glass-materials.clip-right",
div().w(px(68.0)).h(px(56.0)).bg(theme.colors.accent),
),
),
),
)
.child(
backdrop().child(
div().absolute().left(px(20.0)).top(px(24.0)).child(
crate::overlay::surface(
"scene.glass-materials.clip-frame",
&theme,
crate::overlay::OverlaySurface::MODAL,
)
.w(px(160.0))
.h(px(64.0))
.children(
[theme.colors.accent, theme.colors.text_faint]
.map(|color| div().w_full().h(px(32.0)).bg(color)),
),
),
),
),
)
.into_any_element()
}
/// The drawer the scene shows, kept across frames and settled so the capture
/// photographs the panel where it comes to rest rather than mid-slide.
pub(super) struct SceneDrawer {
filters: Entity<Drawer>,
}
impl Global for SceneDrawer {}
pub(super) fn drawer(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneDrawer>() {
let filters = cx.new(|cx| {
Drawer::new("scene.drawer.filters", window, cx)
.edge(Edge::Right)
.size(320.0)
.resizable(true)
.title("Filter runs")
.description("The drawer reports what was chosen. The host applies it.")
.content(|_, cx| {
let theme = cx.theme().clone();
let group = |heading: &'static str, rows: gpui::Div| {
div()
.column()
.gap(px(theme.space(Space::Sm)))
.child(
crate::foundation::text(&theme, TypeScale::Label, heading)
.text_tone(&theme, TextTone::Muted),
)
.child(rows.column().gap(px(theme.space(Space::Sm))))
};
div()
.column()
.gap(px(theme.space(Space::Lg)))
.child(group(
"Outcome",
div()
.child(
Checkbox::new("scene.drawer.failed")
.label("Failed runs only")
.checked(true)
.on_change(|_, _, _| {}),
)
.child(
Checkbox::new("scene.drawer.cancelled")
.label("Include cancelled")
.on_change(|_, _, _| {}),
),
))
.child(group(
"Ownership",
div()
.child(
Checkbox::new("scene.drawer.mine")
.label("Started by me")
.on_change(|_, _, _| {}),
)
.child(
Checkbox::new("scene.drawer.watching")
.label("Repositories I watch")
.checked(true)
.on_change(|_, _, _| {}),
),
))
.child(group(
"Window",
div()
.child(
Checkbox::new("scene.drawer.today")
.label("Today")
.checked(true)
.on_change(|_, _, _| {}),
)
.child(
Checkbox::new("scene.drawer.week")
.label("This week")
.on_change(|_, _, _| {}),
),
))
.into_any_element()
})
// Two controls, sized to their labels and pinned to the
// reading edge: a full-width primary slab is the loudest
// thing on the page and says the drawer has one exit.
.footer(|_, cx| {
let theme = cx.theme().clone();
div()
.row()
.justify_end()
.gap(px(theme.space(Space::Sm)))
.child(
Button::new("scene.drawer.cancel")
.label("Cancel")
.secondary()
.on_click(|_, _| {}),
)
.child(
Button::new("scene.drawer.apply")
.label("Apply")
.primary()
.on_click(|_, _| {}),
)
.into_any_element()
})
});
filters.update(cx, |drawer, cx| {
drawer.open(window, cx);
drawer.settle(cx);
});
cx.set_global(SceneDrawer { filters });
}
let filters = cx.global::<SceneDrawer>().filters.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(620.0))
.h(px(400.0))
.child(page_behind(&theme, "Runs"))
.child(filters)
.into_any_element()
}
pub(super) fn hover_card(window: &mut Window, cx: &mut App) -> AnyElement {
ensure_ordinary(window, cx);
let card = cx.global::<SceneOrdinary>().hover_card.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(460.0))
.h(px(300.0))
.child(caption(&theme, "A preview the pointer can travel into"))
.child(
row(&theme)
.child(crate::foundation::text(
&theme,
TypeScale::Label,
"Reported by",
))
.child(card),
)
.into_any_element()
}
pub(super) fn menubar(window: &mut Window, cx: &mut App) -> AnyElement {
ensure_ordinary(window, cx);
let bar = cx.global::<SceneOrdinary>().menubar.clone();
let theme = cx.theme().clone();
stack(&theme)
.w(px(560.0))
.h(px(360.0))
.child(bar)
.into_any_element()
}
struct SceneBottomSheet(Entity<crate::overlay::sheet::BottomSheet>);
impl Global for SceneBottomSheet {}
pub(super) fn bottom_sheet(window: &mut Window, cx: &mut App) -> AnyElement {
use crate::overlay::sheet::{BottomSheet, BottomSheetEvent, SheetDetent};
if !cx.has_global::<SceneBottomSheet>() {
let sheet = cx.new(|cx| BottomSheet::new("scene.bottom-sheet", window, cx));
sheet.update(cx, |sheet, cx| {
sheet.set_title("Choose destination · fixture", cx);
sheet.set_detents(
vec![
SheetDetent::new("Compact", 290.0),
SheetDetent::new("Half", 430.0),
SheetDetent::new("Full", 640.0),
],
"Half",
cx,
);
let scroll = gpui::ScrollHandle::new();
sheet.set_scroll_handle(Some(scroll.clone()), cx);
sheet.set_content(
Some(Rc::new(move |_, cx| {
let _theme = cx.theme().clone();
div()
.id("scene.bottom-sheet.list")
.column()
.size_full()
.overflow_y_scroll()
.track_scroll(&scroll)
.children(
[
("home", "Home"),
("work", "Work"),
("library", "Library"),
("airport", "Airport"),
("station", "Station"),
("garden", "Garden"),
("museum", "Museum"),
]
.into_iter()
.map(|(id, label)| {
Button::new(format!("scene.bottom-sheet.destination.{id}"))
.label(label)
.secondary()
.control_size(gpui_kit_theme::ControlSize::Touch)
.on_click(|_, _| {})
}),
)
.into_any_element()
})),
cx,
);
sheet.open(window, cx);
sheet.settle(cx);
});
cx.subscribe(&sheet, |sheet, event, cx| {
if let BottomSheetEvent::DetentRequested(id) = event {
sheet.update(cx, |sheet, cx| {
sheet.set_detent(id.clone(), cx);
});
}
})
.detach();
cx.set_global(SceneBottomSheet(sheet));
}
let theme = cx.theme().clone();
stack(&theme)
.w_full()
.h(px(640.0))
.child(page_behind(&theme, "Caller-owned destinations"))
.child(cx.global::<SceneBottomSheet>().0.clone())
.into_any_element()
}
struct SceneActionSheet(Entity<crate::overlay::sheet::ActionSheet>);
impl Global for SceneActionSheet {}
pub(super) fn action_sheet(window: &mut Window, cx: &mut App) -> AnyElement {
use crate::overlay::sheet::{ActionSheet, SheetAction, SheetActionState};
if !cx.has_global::<SceneActionSheet>() {
let sheet = cx.new(|cx| ActionSheet::new("scene.action-sheet", window, cx));
sheet.update(cx, |sheet, cx| {
let mut remove = SheetAction::new("remove", "Remove from collection");
remove.destructive = true;
let mut share = SheetAction::new("share", "Share unavailable");
share.disabled = true;
sheet.set_actions(
vec![SheetAction::new("save", "Save a copy"), share, remove],
cx,
);
sheet.set_state(
SheetActionState::Error(
"Fixture refusal: the last attempt failed; nothing was removed.".into(),
),
cx,
);
sheet.sheet().update(cx, |sheet, cx| {
sheet.set_title("Collection actions · fixture", cx);
});
sheet.open(window, cx);
sheet.sheet().update(cx, |sheet, cx| sheet.settle(cx));
});
cx.set_global(SceneActionSheet(sheet));
}
let theme = cx.theme().clone();
stack(&theme)
.w_full()
.h(px(600.0))
.child(page_behind(&theme, "Last verified collection"))
.child(cx.global::<SceneActionSheet>().0.clone())
.into_any_element()
}
//! Validated conserved flow layouts with persistent node/link labels.
use super::support::*;
use crate::display::plot::{SankeyAlignment, SankeyOrder};
#[derive(Default)]
struct FlowRevisions {
revision: usize,
direct: bool,
selected: Option<SharedString>,
}
pub(super) fn sankey_motion(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let state = crate::motion::keyed::slot::<FlowRevisions>(
&"scene.sankey.motion-state".into(),
window.window_handle().window_id(),
cx,
);
let revision = state.borrow().revision % 3;
let flows = if revision == 0 {
vec![("ad", "a", "d", 70.0), ("bc", "b", "c", 30.0)]
} else if revision == 1 {
vec![("ad", "a", "d", 20.0), ("bc", "b", "c", 80.0)]
} else {
vec![("ad", "a", "d", 45.0)]
};
let mut nodes = Vec::new();
let mut links = Vec::new();
let mut weights = Vec::new();
for (id, source, target, value) in flows {
for node in [source, target] {
nodes.push(SankeyNode::new(
node,
node,
value.to_string(),
Default::default(),
));
}
links.push(SankeyLink::new(
id,
source,
target,
format!("{source} → {target}"),
value.to_string(),
Default::default(),
Default::default(),
0.0,
));
weights.push(value);
}
let (data, _) = SankeyData::new(nodes, links)
.layout_ordered(
&weights,
0.08,
0.1,
SankeyAlignment::Left,
SankeyOrder::Barycenter,
)
.expect("conserved synthetic flow");
let next = state.clone();
let direct = state.clone();
let select = state.clone();
stack(&theme).w(px(600.0)).child(caption(&theme,"Synthetic conserved endpoint layouts · ribbons remain attached during motion; exact current values"))
.child(div().row().gap_token(&theme,Space::Sm)
.child(Button::new("scene.sankey.motion.advance").label("Advance flows").on_click(move |window,_|{next.borrow_mut().revision+=1;window.refresh();}))
.child(Button::new("scene.sankey.motion.direct").label(if state.borrow().direct {"Enable animation"} else {"Direct updates"}).on_click(move |window,_|{let mut s=direct.borrow_mut();s.direct = !s.direct;window.refresh();})))
.child(SankeyChart::new("scene.sankey.motion","Keyed node and ribbon updates",PlotState::Ready(data)).animate(!state.borrow().direct).labels(true)
.selected(state.borrow().selected.clone()).on_current(move |id,window,_|{select.borrow_mut().selected=Some(id);window.refresh();}))
.into_any_element()
}
pub(super) fn sankey_layout(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let graph = SankeyData::new(
[
("a", "Incoming A", "70"),
("b", "Incoming B", "30"),
("c", "Destination C", "30"),
("d", "Destination D", "70"),
]
.map(|(id, label, value)| SankeyNode::new(id, label, value, Default::default())),
[
("ad", "a", "d", "A → D", "70"),
("bc", "b", "c", "B → C", "30"),
]
.map(|(id, source, target, label, value)| {
SankeyLink::new(
id,
source,
target,
label,
value,
Default::default(),
Default::default(),
0.0,
)
}),
);
stack(&theme)
.w(px(860.0))
.child(caption(
&theme,
"Synthetic conserved flows · same 70:30 scale · deterministic crossing reduction",
))
.child(
div().row().gap_token(&theme, Space::Lg).children(
[
(SankeyOrder::Input, "input", "Input order"),
(SankeyOrder::Barycenter, "ordered", "Barycenter order"),
]
.map(|(order, id, label)| {
let (data, _) = graph
.clone()
.layout_ordered(&[70.0, 30.0], 0.08, 0.1, SankeyAlignment::Left, order)
.expect("valid flows");
div().w(px(380.0)).child(
SankeyChart::new(
Ident::new("scene.sankey").child(id),
label,
PlotState::Ready(data),
)
.labels(true)
.on_current(|_, _, _| {}),
)
}),
),
)
.into_any_element()
}
pub(super) fn raw_candlestick(_window: &mut Window, cx: &mut App) -> AnyElement {
use crate::display::chart::{
cartesian::CartesianChart,
data::{ChartScale, ChartValue, RawPoint, RawSeries, SeriesMark, ValueAxis},
scale::{NumericScale, ScaleKind},
};
use crate::display::plot::RawOhlc;
let theme = cx.theme().clone();
let day = 86_400_000.0;
let x = ChartScale::Numeric(
NumericScale::new(ScaleKind::Time, [0.0, 4.0 * day]).expect("time domain"),
);
let candles = RawOhlc::series(
"ohlc",
"price",
[
RawOhlc::new(
"day-a",
day,
[30.0, 60.0, 20.0, 50.0],
"Day A",
"O30 H60 L20 C50",
),
RawOhlc::new(
"day-b",
day * 2.0,
[65.0, 75.0, 35.0, 40.0],
"Day B",
"O65 H75 L35 C40",
),
RawOhlc::new(
"day-c",
day * 3.0,
[45.0, 55.0, 30.0, 45.0],
"Day C · doji",
"O45 H55 L30 C45",
),
],
theme.colors.success,
theme.colors.danger,
)
.expect("validated OHLC");
let volume = RawSeries::new("volume", "volume", SeriesMark::Bar)
.tint(theme.colors.accent.opacity(0.22))
.points(
[
("day-a", day, 120.0),
("day-b", 2.0 * day, 260.0),
("day-c", 3.0 * day, 80.0),
]
.map(|(id, x, value)| RawPoint::new(id, ChartValue::Number(x), Some(value))),
);
let overlay = RawSeries::new("reference", "price", SeriesMark::Line)
.tint(theme.colors.warning)
.points(
[
("day-a", day, 42.0),
("day-b", 2.0 * day, 48.0),
("day-c", 3.0 * day, 46.0),
]
.map(|(id, x, value)| RawPoint::new(id, ChartValue::Number(x), Some(value))),
);
stack(&theme).w(px(900.0)).child(caption(&theme, "Synthetic OHLC · shared UTC-duration x scale · independent volume axis · caller overlay"))
.child(CartesianChart::new("scene.raw-ohlc", "OHLC with volume and overlay", x, [
ValueAxis { id: "price".into(), label: "Price".into(), scale: NumericScale::new(ScaleKind::Linear, [0.0, 80.0]).expect("price domain") },
ValueAxis { id: "volume".into(), label: "Volume".into(), scale: NumericScale::new(ScaleKind::Linear, [0.0, 300.0]).expect("volume domain") },
]).series([volume, candles, overlay]).format_ticks(move |axis, value| if axis == "x" { format!("D{:.0}", value / day).into() } else { value.to_string().into() }).selected(Some(ChartSelection::new("ohlc", "day-b"))).on_event(|_, _, _| {}))
.into_any_element()
}
//! Raw-value specialized visualization exhibits, using original synthetic data.
use super::support::*;
use crate::display::specialized::{
HierarchyNode, SpecializedChart, SpecializedData, WeightedValue,
};
#[derive(Default)]
struct Exploration {
revision: usize,
focus: Option<SharedString>,
selected: Option<SharedString>,
refuse: bool,
}
pub(super) fn specialized_exploration(window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let state = crate::motion::keyed::slot::<Exploration>(
&"scene.specialized.explore-state".into(),
window.window_handle().window_id(),
cx,
);
let revision = state.borrow().revision;
let selected = state.borrow().selected.clone();
let focus = state
.borrow()
.focus
.clone()
.unwrap_or_else(|| "total".into());
let root = HierarchyNode::branch(
"total",
"All samples",
vec![
HierarchyNode::branch(
"group",
"Processing",
vec![
HierarchyNode::leaf(WeightedValue::new(
"parse",
"Parsing",
if revision.is_multiple_of(2) {
12.0
} else {
47.0
},
)),
HierarchyNode::leaf(WeightedValue::new(
"execute",
"Execution",
if revision.is_multiple_of(2) {
68.0
} else {
13.0
},
)),
],
),
HierarchyNode::leaf(WeightedValue::new("deliver", "Delivery", 20.0)),
],
);
let next = state.clone();
let refusal = state.clone();
let charts = [
(
"tree",
"Area · drill/back",
SpecializedData::treemap_at(&root, &focus),
),
(
"sun",
"Angle · drill/back",
SpecializedData::sunburst_at(&root, &focus),
),
(
"funnel",
"Width · enter/update/exit",
SpecializedData::funnel(match revision % 3 {
0 => vec![
WeightedValue::new("start", "Started", 120.0),
WeightedValue::new("finish", "Finished", 24.0),
],
1 => vec![
WeightedValue::new("start", "Started", 75.0),
WeightedValue::new("new", "New sample", 38.0),
],
_ => vec![],
}),
),
];
stack(&theme).w(px(860.0))
.child(caption(&theme,"Synthetic live fixtures · advance during motion to interrupt; empty exit shapes are not selectable"))
.child(div().row().gap_token(&theme,Space::Sm)
.child(Button::new("scene.specialized.advance").label("Advance samples").on_click(move |window,_| { next.borrow_mut().revision+=1; window.refresh(); }))
.child(Button::new("scene.specialized.refuse").label(if state.borrow().refuse { "Accept proposals" } else { "Refuse proposals" }).on_click(move |window,_| { let mut s=refusal.borrow_mut(); s.refuse = !s.refuse; window.refresh(); })))
.child(div().row().gap_token(&theme,Space::Lg).children(charts.into_iter().map(|(id,label,data)| {
let select = state.clone();
let navigate = state.clone();
div().w(px(260.0)).child(SpecializedChart::new(Ident::new("scene.specialized.live").child(id),label,PlotState::Ready(data.expect("valid exploration fixture")))
.motion(true).labels(true).selected(selected.clone())
.on_current(move |id,window,_| { let mut s=select.borrow_mut(); if !s.refuse { s.selected=Some(id); } window.refresh(); })
.on_navigate(move |id,window,_| { let mut s=navigate.borrow_mut(); if !s.refuse { s.focus=Some(id); } window.refresh(); }))
}))).into_any_element()
}
pub(super) fn specialized(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let leaf = |id, label, value| HierarchyNode::leaf(WeightedValue::new(id, label, value));
let root = HierarchyNode::branch(
"total",
"All work",
vec![
leaf("build", "Build", 60.0),
HierarchyNode::branch(
"verify",
"Verification",
vec![leaf("tests", "Tests", 30.0), leaf("lint", "Lint", 10.0)],
),
],
);
let fixtures = [
(
"treemap",
"Treemap · area = work",
SpecializedData::treemap(&root),
),
(
"sunburst",
"Sunburst · angle = work",
SpecializedData::sunburst(&root),
),
(
"funnel",
"Funnel · width = count",
SpecializedData::funnel(vec![
WeightedValue::new("seen", "Seen", 120.0),
WeightedValue::new("started", "Started", 75.0),
WeightedValue::new("done", "Completed", 24.0),
]),
),
];
stack(&theme)
.w(px(940.0))
.child(caption(
&theme,
"Synthetic hierarchy fixtures · select marks with arrow keys or value keys",
))
.child(
div()
.row()
.gap_token(&theme, Space::Lg)
.children(fixtures.into_iter().map(|(id, label, data)| {
div().w(px(280.0)).child(
SpecializedChart::new(
Ident::new("scene.specialized").child(id),
label,
PlotState::Ready(data.expect("valid hierarchy")),
)
.on_current(|_, _, _| {}),
)
})),
)
.child(
div().w(px(220.0)).child(SpecializedChart::new(
"scene.specialized.zero",
"Zero is measured, not missing",
PlotState::Stale {
data: SpecializedData::funnel(vec![WeightedValue::new(
"zero",
"Measured zero",
0.0,
)])
.expect("zero"),
reason: "Refresh refused; verified zero retained".into(),
},
)),
)
.into_any_element()
}
pub(super) fn specialized_distribution(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
let fixtures = [
(
"histogram",
"Histogram · count per bin",
SpecializedData::histogram(&[-2.0, -1.0, 0.0, 0.0, 1.0, 3.0], &[-2.0, 0.0, 1.0, 3.0]),
),
(
"box",
"Box · R-7 / Tukey · domain 0–110",
SpecializedData::box_plot(
"latency",
"Latency",
&[1.0, 2.0, 4.0, 7.0, 8.0, 100.0],
[0.0, 110.0],
),
),
(
"range",
"Range · domain −10–30",
SpecializedData::range("interval", "Expected interval", -4.0, 18.0, [-10.0, 30.0]),
),
(
"waterfall",
"Waterfall · signed changes",
SpecializedData::waterfall(vec![
WeightedValue::new("open", "Opening", 60.0),
WeightedValue::new("cost", "Cost", -90.0),
WeightedValue::new("credit", "Credit", 10.0),
]),
),
];
stack(&theme)
.w(px(1000.0))
.child(caption(
&theme,
"Synthetic distribution fixtures · explicit bin edges and R-7 / Tukey statistics",
))
.child(
div()
.row()
.flex_wrap()
.gap_token(&theme, Space::Lg)
.children(fixtures.into_iter().map(|(id, label, data)| {
div().w(px(440.0)).child(
SpecializedChart::new(
Ident::new("scene.specialized").child(id),
label,
PlotState::Ready(data.expect("valid synthetic fixture")),
)
.on_current(|_, _, _| {}),
)
})),
)
.into_any_element()
}
//! A schema, rendered as data or as a form.
use super::support::*;
/// A document with the three facts a viewer usually confuses: a key holding
/// `null`, a key holding an empty object, and a key that is simply not here.
pub(super) fn scene_document() -> JsonValue {
JsonValue::object([
("id", JsonValue::string("run-4812")),
("attempts", JsonValue::number("3")),
("streaming", JsonValue::Bool(true)),
// Nothing has been recorded here yet, which is not the same as the
// key being missing: `resumed_from` is missing, and says nothing.
("cursor", JsonValue::Null),
("labels", JsonValue::object(Vec::<(&str, JsonValue)>::new())),
(
"credentials",
JsonValue::object([("token", JsonValue::redacted("51 characters"))]),
),
(
"request",
JsonValue::object([
("method", JsonValue::string("POST")),
(
"headers",
JsonValue::object([
("content-type", JsonValue::string("application/json")),
("authorization", JsonValue::redacted("a value")),
]),
),
]),
),
(
"steps",
JsonValue::array([
JsonValue::string("plan"),
JsonValue::string("apply"),
JsonValue::object([("retries", JsonValue::number("0"))]),
]),
),
])
}
pub(super) fn json_view(_window: &mut Window, cx: &mut App) -> AnyElement {
let theme = cx.theme().clone();
stack(&theme)
.w(px(520.0))
.child(
JsonView::new("scene.json", scene_document())
.expanded_paths(&["credentials", "request", "request/headers", "steps"])
.selected("request/method")
.on_toggle(|_, _, _, _| {})
.on_select(|_, _, _| {}),
)
.into_any_element()
}
/// The form kept across frames.
///
/// Its fields own carets, open menus and a selection, so rebuilding it every
/// frame would throw away whatever had been typed into it.
pub(super) struct SceneSchemaForm {
form: Entity<SchemaForm>,
}
impl Global for SceneSchemaForm {}
pub(super) fn scene_schema() -> Schema {
Schema::new()
.field(
SchemaField::new(
"path",
SchemaKind::Text {
placeholder: Some("relative to the workspace".into()),
secret: false,
},
)
.label("File")
.description("Which file the call reads")
.required(true),
)
.field(
SchemaField::new(
"max_bytes",
SchemaKind::Integer(NumberBounds::new().min(1.0).max(65_536.0).step(1024.0)),
)
.label("Maximum bytes"),
)
.field(
SchemaField::new("follow_symlinks", SchemaKind::Boolean).label("Follow symbolic links"),
)
.field(
SchemaField::new(
"encoding",
SchemaKind::Enum(vec![
SchemaChoice::new("utf-8", "UTF-8"),
SchemaChoice::new("latin-1", "Latin-1"),
]),
)
.label("Encoding")
.required(true),
)
.field(
SchemaField::new("attachments", SchemaKind::Files { max: Some(2) })
.label("Attachments")
.description("Paths are acquired and accepted by the host"),
)
.field(
SchemaField::new(
"headers",
SchemaKind::List {
item: Box::new(SchemaField::new(
"header",
SchemaKind::Object(vec![
SchemaField::new(
"name",
SchemaKind::Text {
placeholder: Some("Header name".into()),
secret: false,
},
)
.label("Name")
.required(true),
SchemaField::new("enabled", SchemaKind::Boolean).label("Enabled"),
]),
)),
max: Some(3),
},
)
.label("Headers"),
)
.field(
SchemaField::new(
"limits",
SchemaKind::Object(vec![
SchemaField::new("timeout_ms", SchemaKind::Integer(NumberBounds::new()))
.label("Timeout in milliseconds"),
]),
)
.label("Limits"),
)
.field(
SchemaField::new(
"matcher",
SchemaKind::Unrenderable(
"This argument is one of three shapes at once, and no single control \
stands for that."
.into(),
),
)
.label("Matcher")
.required(true),
)
}
pub(super) fn schema_form(window: &mut Window, cx: &mut App) -> AnyElement {
if !cx.has_global::<SceneSchemaForm>() {
set_schema_file_policy(DefaultSchemaFilePolicy, cx);
let form = cx.new(|cx| SchemaForm::new("scene.schema", scene_schema(), window, cx));
form.update(cx, |form, cx| {
// An error the host returned rather than one the form derived:
// only the host knows this path is outside the workspace.
form.set_error("path", "That path is outside the workspace.", cx);
let _ = form.set_field_validation("encoding", ValidationState::Validating, cx);
form.set_files("attachments", vec!["/workspace/brief.md".into()], cx)
.expect("the scene's permissive file policy accepts its fixture");
form.add_list_item("headers", window, cx);
});
cx.set_global(SceneSchemaForm { form });
}
let form = cx.global::<SceneSchemaForm>().form.clone();
let theme = cx.theme().clone();
stack(&theme).w(px(520.0)).child(form).into_any_element()
}
//! What more than one family of scenes needs.
//!
//! Everything here is used by scenes in at least two families. A fixture
//! that only one family builds lives in that family, next to the scenes
//! that read it, so that moving a component moves its fixture with it.
pub(super) use gpui::{
AnyElement, App, Corners, DevicePixels, Entity, Focusable, Global, IntoElement, Pixels,
RenderImage, SharedString, SpriteBlendMode, SpriteColorMode, SpriteInstance, SpriteTransform,
Window, bounds, canvas, conic_gradient_stops, div, linear_color_stop, point, prelude::*, px,
radial_gradient_stops, radians, size,
};
pub(super) use gpui_kit_assets::Icon;
pub(super) use gpui_kit_semantics::{NodeSpec, Role, Semantic};
pub(super) use gpui_kit_theme::{Radius, Space, Surface, SyntaxColor, TextTone, Theme, TypeScale};
pub(super) use std::rc::Rc;
pub(super) use std::sync::{Arc, OnceLock};
pub(super) use std::time::Duration;
pub(super) use crate::controls::combobox::Combobox;
pub(super) use crate::controls::input::TextInput;
pub(super) use crate::controls::keybinding_recorder::KeybindingRecorder;
pub(super) use crate::controls::number_input::NumberInput;
pub(super) use crate::controls::select::{Select, SelectOption};
pub(super) use crate::controls::split_button::SplitButton;
pub(super) use crate::controls::tag_input::TagInput;
pub(super) use crate::controls::textarea::{Frame, TextArea};
pub(super) use crate::display::badge::Tone;
pub(super) use crate::display::icon::{Icon as IconView, IconTone};
pub(super) use crate::foundation::ActiveTheme;
pub(super) use crate::foundation::direction::{ActiveDirection, DirectionalExt};
pub(super) use crate::interaction::dnd;
pub(super) use crate::overlay::toast::push as toast_push;
pub(super) use crate::overlay::{Edge, Kbd, Overlay, Placement, Tooltip, Tooltipped};
pub(super) use crate::prelude::*;
pub(super) use crate::strings::{ActiveStrings, StringKey};
pub(super) fn stack(theme: &Theme) -> gpui::Div {
div()
.column()
.gap(px(theme.spacing.md))
.p(px(theme.spacing.lg))
.page(theme)
.font_family(theme.typography.sans.clone())
}
pub(super) fn row(theme: &Theme) -> gpui::Div {
div()
.row()
.flex_wrap()
.gap(px(theme.spacing.sm))
.items_center()
}
/// A pane of fixture copy, for a scene that needs something on both sides of a
/// divider or below a viewport.
pub(super) fn filler(theme: &Theme, title: &'static str, lines: usize) -> gpui::Div {
let mut pane = div()
.flex()
.flex_col()
.gap(px(theme.space(Space::Xs)))
.p(px(theme.space(Space::Md)))
.child(crate::foundation::text(
theme,
TypeScale::Label,
SharedString::from(title),
));
for line in 1..=lines {
pane = pane.child(
crate::foundation::text(
theme,
TypeScale::Code,
SharedString::from(format!("Fixture line {line:02}")),
)
.text_tone(theme, TextTone::Muted),
);
}
pane
}
/// A caption naming the state a drag scene was staged in.
pub(super) fn caption(theme: &Theme, text: impl Into<SharedString>) -> gpui::Div {
crate::foundation::text(theme, TypeScale::Caption, text.into())
.text_tone(theme, TextTone::Muted)
}
/// The element a host hands back for an image this crate did not fetch.
///
/// A flat fill rather than a picture, because the crate ships no photographs
/// and a scene has to photograph the same pixels every run. Neutral, and
/// quieter than the surface it sits in: a stand-in painted in the accent was
/// the loudest thing in every scene that used one, which said the placeholder
/// mattered more than the component being reviewed around it.
pub(super) fn scene_picture(label: &'static str, cx: &App) -> AnyElement {
let theme = cx.theme().clone();
div()
.size_full()
.flex()
.flex_col()
.items_center()
.justify_center()
.gap(px(theme.space(gpui_kit_theme::Space::Xs)))
.bg(theme.colors.sunken)
.child(
gpui_kit_assets::icon(gpui_kit_assets::Icon::Image)
.size(px(theme.typography.subtitle.line_height))
.text_color(theme.colors.text_faint),
)
.child(
crate::foundation::text(&theme, TypeScale::Caption, SharedString::new_static(label))
.text_tone(&theme, TextTone::Faint),
)
.into_any_element()
}
/// The ordinary-application views that own state across frames.
pub(super) struct SceneOrdinary {
pub(super) menubar: Entity<Menubar>,
pub(super) hover_card: Entity<HoverCard>,
pub(super) copy_idle: Entity<CopyButton>,
pub(super) copy: Entity<CopyButton>,
pub(super) copy_refused: Entity<CopyButton>,
}
impl Global for SceneOrdinary {}
pub(super) fn ensure_ordinary(window: &mut Window, cx: &mut App) {
if cx.has_global::<SceneOrdinary>() {
return;
}
let menubar = cx.new(|cx| {
Menubar::new(
"scene.menubar",
[
MenubarMenu::new(
"file",
"File",
[
MenuItem::command("file.new", "New run").shortcut("cmd-n"),
MenuItem::command("file.open", "Open workspace").shortcut("cmd-o"),
MenuItem::separator("file.rule"),
MenuItem::submenu(
"file.export",
"Export",
[
MenuItem::command("file.export.json", "As JSON"),
MenuItem::command("file.export.text", "As plain text"),
],
),
],
),
MenubarMenu::new(
"edit",
"Edit",
[
MenuItem::command("edit.undo", "Undo").shortcut("cmd-z"),
MenuItem::check("edit.wrap", "Wrap lines", true),
],
),
MenubarMenu::new("view", "View", [MenuItem::command("view.zoom", "Zoom in")]),
MenubarMenu::new("policy", "Policy", []).disabled(true),
],
window,
cx,
)
});
menubar.update(cx, |bar, cx| bar.open("file", window, cx));
let hover_card = cx.new(|cx| {
HoverCard::new("scene.hover-card", window, cx)
.name("Run 4821")
.trigger(|_, cx| {
let theme = cx.theme().clone();
crate::foundation::text(&theme, TypeScale::Caption, "run 4821")
.text_color(theme.colors.accent)
.into_any_element()
})
.content(|_, cx| {
let theme = cx.theme().clone();
div()
.column()
// A column stretches its children across, and a badge
// stretched that far stops reading as a label and starts
// reading as a filled bar.
.items_start()
.gap(px(theme.spacing.xs))
.child(crate::foundation::text(
&theme,
TypeScale::Strong,
"Nightly regression sweep",
))
.child(
crate::foundation::text(
&theme,
TypeScale::Caption,
"Finished in 4 minutes, 12 checks, none failed.",
)
.text_tone(&theme, TextTone::Muted),
)
.child(Badge::new("Ready").success())
.into_any_element()
})
});
hover_card.update(cx, |card, cx| card.open(cx));
// Nobody has pressed this one. It is the state a reader sees almost all
// of the time, and it was the one state of the three no image held.
let copy_idle = cx.new(|cx| {
CopyButton::new("scene.copy-idle", window, cx)
.text("run-4821-9f3a")
.copier(|_, _| Ok(()))
});
// The confirmation outlives the capture. At its ordinary length it expires
// partway through a run, so which of the two states this scene showed
// depended on how long the run took to reach it.
let copy = cx.new(|cx| {
CopyButton::new("scene.copy", window, cx)
.text("run-4821-9f3a")
.confirmation(std::time::Duration::from_secs(60 * 60))
.copier(|_, _| Ok(()))
});
copy.update(cx, |button, cx| button.copy(cx));
let copy_refused = cx.new(|cx| {
CopyButton::new("scene.copy-refused", window, cx)
.text("run-4821-9f3a")
.copier(|_, _| Err("The clipboard did not take it.".into()))
});
copy_refused.update(cx, |button, cx| button.copy(cx));
cx.set_global(SceneOrdinary {
menubar,
hover_card,
copy_idle,
copy,
copy_refused,
});
}