Files
Ely-GPUI-Components/src/data_display/changelog.rs
T

181 lines
5.4 KiB
Rust

use gpui::{
App, FontWeight, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
prelude::*,
};
use super::{Badge, Timeline, TimelineItem, Tone};
use crate::theme::ActiveTheme;
/// A kind of change, as Keep a Changelog names them.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Change {
Added,
Changed,
Deprecated,
Removed,
Fixed,
Security,
}
impl Change {
fn label(self) -> &'static str {
match self {
Self::Added => "Added",
Self::Changed => "Changed",
Self::Deprecated => "Deprecated",
Self::Removed => "Removed",
Self::Fixed => "Fixed",
Self::Security => "Security",
}
}
fn tone(self) -> Tone {
match self {
Self::Added => Tone::Success,
Self::Changed => Tone::Info,
Self::Deprecated => Tone::Warning,
Self::Removed | Self::Security => Tone::Danger,
Self::Fixed => Tone::Neutral,
}
}
}
/// One version: its date, a line about it, and its changes.
pub struct Release {
version: SharedString,
date: SharedString,
summary: Option<SharedString>,
changes: Vec<(Change, SharedString)>,
}
impl Release {
pub fn new(version: impl Into<SharedString>, date: impl Into<SharedString>) -> Self {
Self {
version: version.into(),
date: date.into(),
summary: None,
changes: Vec::new(),
}
}
pub fn summary(mut self, summary: impl Into<SharedString>) -> Self {
self.summary = Some(summary.into());
self
}
pub fn change(mut self, kind: Change, text: impl Into<SharedString>) -> Self {
self.changes.push((kind, text.into()));
self
}
}
/// Changes grouped by kind, kinds in Keep a Changelog order, each kind's changes as given.
fn grouped(changes: Vec<(Change, SharedString)>) -> Vec<(Change, Vec<SharedString>)> {
let mut groups: Vec<(Change, Vec<SharedString>)> = Vec::new();
for (kind, text) in changes {
match groups.iter_mut().find(|(seen, _)| *seen == kind) {
Some((_, texts)) => texts.push(text),
None => groups.push((kind, vec![text])),
}
}
groups.sort_by_key(|(kind, _)| *kind);
groups
}
/// Releases down a rail in the order given; the first reads Latest.
#[derive(IntoElement, Default)]
pub struct Changelog {
releases: Vec<Release>,
}
impl Changelog {
pub fn new() -> Self {
Self::default()
}
pub fn release(mut self, release: Release) -> Self {
self.releases.push(release);
self
}
}
impl RenderOnce for Changelog {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let colors = &cx.theme().colors;
let (fg, subtle) = (colors.fg, colors.fg_subtle);
self.releases
.into_iter()
.enumerate()
.fold(Timeline::new(), |timeline, (ix, release)| {
let head = div()
.flex()
.items_center()
.gap_2()
.font_weight(FontWeight::SEMIBOLD)
.child(release.version)
.when(ix == 0, |head| {
head.child(Badge::new("Latest").tone(Tone::Accent))
});
let groups = grouped(release.changes).into_iter().map(|(kind, texts)| {
div()
.flex()
.flex_col()
.items_start()
.gap_1()
.mt_3()
.child(Badge::new(kind.label()).tone(kind.tone()))
.children(texts.into_iter().map(|text| {
div()
.flex()
.gap_2()
.text_color(fg)
.child(div().text_color(subtle).child("•"))
.child(div().flex_1().min_w_0().child(text))
}))
});
timeline.item(
TimelineItem::new(head)
.tone(if ix == 0 { Tone::Accent } else { Tone::Neutral })
.time(release.date)
.children(release.summary)
.children(groups),
)
})
}
}
#[cfg(test)]
mod tests {
use super::{Change, grouped};
#[test]
fn changes_group_by_kind_in_changelog_order() {
let changes = vec![
(Change::Fixed, "a crash".into()),
(Change::Added, "a timeline".into()),
(Change::Fixed, "a typo".into()),
(Change::Removed, "the old feed".into()),
];
let groups: Vec<_> = grouped(changes)
.into_iter()
.map(|(kind, texts)| {
(
kind,
texts.iter().map(|t| t.to_string()).collect::<Vec<_>>(),
)
})
.collect();
assert_eq!(
groups,
vec![
(Change::Added, vec!["a timeline".to_string()]),
(Change::Removed, vec!["the old feed".to_string()]),
(
Change::Fixed,
vec!["a crash".to_string(), "a typo".to_string()]
),
]
);
}
}