use ely_gpui_component::{ buttons::{Button, ButtonVariant, SegmentedControl}, data_display::{ Activity, ActivityFeed, Badge, Change, Changelog, DescriptionList, PropertyGrid, PropertyGroup, Release, Timeline, TimelineItem, UserChip, }, forms::{NumberInput, Slider, Switch}, primitives::{IconName, Severity}, theme::{ActiveTheme, ControlSize, Radius}, typography::Code, }; use gpui::{ App, Entity, IntoElement, ParentElement, SharedString, Styled, Window, div, prelude::*, px, }; use jiff::{Timestamp, ToSpan}; use crate::{ probe::probe, ui::{keep, section, set}, }; pub fn descriptions(cx: &mut App) -> impl IntoElement + use<> { section( "DescriptionList / KeyValueList / InfoRow", "Labels and their values: in a quiet column beside them, or stacked where room is narrow.", cx, ) .child( div() .flex() .items_start() .gap_12() .child( div().flex_1().child( DescriptionList::new() .lined() .item("Status", Badge::new("Paid").tone(Severity::Success).dot()) .item( "Customer", UserChip::new("desc-customer", "Katherine Johnson"), ) .item("Amount", "$1,280.00") .item("Placed", "Sep 24, 2026 ยท 14:32") .item("Reference", Code::new("ORD-2419")), ), ) .child( div().w_64().child( DescriptionList::new() .stacked() .item("Ship to", "12 Harbour Street, Wellington 6011, New Zealand") .item("Delivery", "Standard, three to five days"), ), ), ) } #[derive(Clone)] struct Props { width: f64, height: f64, direction: SharedString, opacity: f64, visible: bool, } fn edit( props: &Entity, apply: fn(&mut Props, V), ) -> impl Fn(V, &mut Window, &mut App) + 'static { let props = props.clone(); move |value, _, cx| { props.update(cx, |props, cx| { apply(props, value); cx.notify(); }) } } pub fn properties(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let props = keep( "props", || Props { width: 240.0, height: 144.0, direction: "row".into(), opacity: 0.8, visible: true, }, window, cx, ); let now = props.read(cx).clone(); let theme = cx.theme(); let colors = &theme.colors; let direction = props.clone(); let grid = PropertyGrid::new("props-grid") .group( PropertyGroup::new("Layout") .row( "Width", NumberInput::new("prop-width", now.width) .range(40.0, 320.0) .size(ControlSize::Sm) .on_change(edit(&props, |p, v| p.width = v)), ) .row( "Height", NumberInput::new("prop-height", now.height) .range(40.0, 200.0) .size(ControlSize::Sm) .on_change(edit(&props, |p, v| p.height = v)), ) .row( "Direction", SegmentedControl::new("prop-direction", now.direction.clone()) .segment("row", "Row", None) .segment("column", "Column", None) .size(ControlSize::Sm) .on_change(move |value, _, cx| { direction.update(cx, |props, cx| { props.direction = value.clone(); cx.notify(); }) }), ), ) .group( PropertyGroup::new("Appearance") .row( "Opacity", Slider::new("prop-opacity", now.opacity) .range(0.0, 1.0) .step(0.01) .on_change(edit(&props, |p, v| p.opacity = v)), ) .row( "Visible", Switch::new("prop-visible", now.visible) .on_change(edit(&props, |p, v| p.visible = v)), ), ) .group( PropertyGroup::new("Export").folded().row( "Scale", NumberInput::new("prop-scale", 2.0) .range(1.0, 4.0) .size(ControlSize::Sm), ), ); let bar = |ix: usize| { div() .flex_1() .rounded(theme.radius(Radius::Sm)) .bg(colors.accent.opacity(0.35 + 0.2 * ix as f32)) }; let frame = div() .flex() .when(now.direction == "column", |frame| frame.flex_col()) .gap_2() .p_2() .w(px(now.width as f32)) .h(px(now.height as f32)) .rounded(theme.radius(Radius::Md)) .border_1() .border_color(colors.accent) .opacity(now.opacity as f32) .children((0..3).map(bar)); section( "PropertyGrid", "An inspector: groups fold, each row a name and its editor. The owner keeps the values; the frame on the right follows them.", cx, ) .child( div() .flex() .items_start() .gap_6() .child( div() .w_80() .flex_none() .border_1() .border_color(colors.border) .rounded(theme.radius(Radius::Md)) .child(grid), ) .child( div() .flex_1() .h_64() .flex() .items_center() .justify_center() .rounded(theme.radius(Radius::Md)) .bg(colors.sunken) .when(now.visible, |stage| stage.child(frame)), ), ) } pub fn timelines(cx: &mut App) -> impl IntoElement + use<> { let deploy = Timeline::new() .item( TimelineItem::new("Build started") .time("14:02") .icon(IconName::GitCommitHorizontal) .child("main at 4f2a9c1, by Radia Perlman"), ) .item( TimelineItem::new("Tests passed") .time("14:06") .icon(IconName::Check) .tone(Severity::Success) .child("412 tests in 3 min 12 s"), ) .item( TimelineItem::new("Deployed to staging") .time("14:09") .icon(IconName::Rocket) .tone(Severity::Info), ) .item( TimelineItem::new("Promote to production") .icon(IconName::Hourglass) .child("Waiting for approval"), ); let project = Timeline::new() .item(TimelineItem::new("Kickoff").time("Jan 12")) .item( TimelineItem::new("Design review") .time("Feb 3") .child("Type scale settled; spacing loosened by one step."), ) .item( TimelineItem::new("Beta") .time("Mar 18") .tone(Severity::Success), ) .item( TimelineItem::new("Launch") .time("Apr 30") .tone(Severity::Warning) .child("Planned"), ); section( "Timeline / TimelineItem", "Events down a rail, marked by an icon in a tinted ring or by a dot, with detail below.", cx, ) .child( div() .flex() .items_start() .gap_12() .child(div().flex_1().child(deploy)) .child(div().flex_1().child(project)), ) } /// The canned arrivals the demo cycles through: actor, verb, object. const ARRIVALS: [(&str, &str, &str); 3] = [ ("Sophie Wilson", "commented on", "Release notes"), ("Tim Berners-Lee", "linked", "Spec draft"), ("Radia Perlman", "approved", "Promote to production"), ]; pub fn feed(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> { let opened = *keep("feed-opened", Timestamp::now, window, cx).read(cx); let arrivals = keep("feed-arrivals", Vec::::new, window, cx); let (now, post) = (arrivals.read(cx).clone(), arrivals.clone()); let ago = |span: jiff::Span| opened.checked_sub(span).expect("a time in range"); let feed = now .iter() .enumerate() .rev() .fold(ActivityFeed::new("feed"), |feed, (ix, at)| { let (actor, verb, object) = ARRIVALS[ix % ARRIVALS.len()]; feed.entry(Activity::new(format!("arrival-{ix}"), actor, verb, *at).object(object)) }); let feed = feed .entry( Activity::new("comment", "Grace Hopper", "commented on", ago(3.minutes())) .object("Design review") .body("The cards finally breathe. Ship it."), ) .entry( Activity::new("deploy", "Build bot", "deployed", ago(2.hours())) .object("v0.4.0") .icon(IconName::Rocket), ) .entry( Activity::new("merge", "Alan Turing", "merged", ago(26.hours())) .object("Timeline component"), ) .entry( Activity::new("create", "Ada Lovelace", "created", ago(72.hours())) .object("Q4 roadmap"), ); section( "ActivityFeed", "Who did what, and when, newest first. A new entry folds in at the top; the times stay fresh.", cx, ) .child(div().max_w(px(560.)).child(feed)) .child(div().flex().child(probe( "feed-post", Button::new("feed-post", "Something happens") .variant(ButtonVariant::Ghost) .icon(IconName::Plus) .on_click(move |_, _, cx| { let mut next = post.read(cx).clone(); next.push(Timestamp::now()); set(&post, next, cx) }), ))) } pub fn changelog(cx: &mut App) -> impl IntoElement + use<> { section( "Changelog", "Releases, newest first, each change under its kind.", cx, ) .child( div().max_w(px(560.)).child( Changelog::new() .release( Release::new("2.4.0", "Sep 25, 2026") .summary("Faster search, and a calmer sidebar.") .change(Change::Added, "Search inside attachments.") .change(Change::Fixed, "Pasting a table keeps its borders.") .change(Change::Added, "Pin a note to the sidebar.") .change(Change::Changed, "The sidebar folds with a narrow window."), ) .release( Release::new("2.3.1", "Sep 9, 2026") .change(Change::Fixed, "Sync resumes after sleep.") .change(Change::Security, "Shared links expire when they should."), ) .release( Release::new("2.3.0", "Aug 28, 2026") .change(Change::Added, "Templates for new notes.") .change(Change::Deprecated, "The legacy export format.") .change(Change::Removed, "The old toolbar."), ), ), ) }