Add progress bar component (#28518)
- Adds the progress bar component Release Notes: - N/A
This commit is contained in:
parent
b0b52f299c
commit
3abf95216c
12 changed files with 377 additions and 18 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -65,6 +65,7 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
|
"component",
|
||||||
"context_server",
|
"context_server",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.8.0",
|
||||||
"db",
|
"db",
|
||||||
|
@ -85,6 +86,7 @@ dependencies = [
|
||||||
"language",
|
"language",
|
||||||
"language_model",
|
"language_model",
|
||||||
"language_model_selector",
|
"language_model_selector",
|
||||||
|
"linkme",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
"markdown",
|
"markdown",
|
||||||
|
|
|
@ -32,6 +32,7 @@ client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
|
component.workspace = true
|
||||||
context_server.workspace = true
|
context_server.workspace = true
|
||||||
convert_case.workspace = true
|
convert_case.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
|
@ -51,6 +52,7 @@ itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
language_model_selector.workspace = true
|
language_model_selector.workspace = true
|
||||||
|
linkme.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lsp.workspace = true
|
lsp.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
|
@ -85,9 +87,9 @@ ui.workspace = true
|
||||||
ui_input.workspace = true
|
ui_input.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
workspace-hack.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
mod context_pill;
|
mod context_pill;
|
||||||
|
mod user_spending;
|
||||||
|
|
||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use context_pill::*;
|
pub use context_pill::*;
|
||||||
|
// pub use user_spending::*;
|
||||||
|
|
186
crates/agent/src/ui/user_spending.rs
Normal file
186
crates/agent/src/ui/user_spending.rs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
use gpui::{Entity, Render};
|
||||||
|
use ui::{ProgressBar, prelude::*};
|
||||||
|
|
||||||
|
#[derive(RegisterComponent)]
|
||||||
|
pub struct UserSpending {
|
||||||
|
free_tier_current: u32,
|
||||||
|
free_tier_cap: u32,
|
||||||
|
over_tier_current: u32,
|
||||||
|
over_tier_cap: u32,
|
||||||
|
free_tier_progress: Entity<ProgressBar>,
|
||||||
|
over_tier_progress: Entity<ProgressBar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserSpending {
|
||||||
|
pub fn new(
|
||||||
|
free_tier_current: u32,
|
||||||
|
free_tier_cap: u32,
|
||||||
|
over_tier_current: u32,
|
||||||
|
over_tier_cap: u32,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Self {
|
||||||
|
let free_tier_capped = free_tier_current == free_tier_cap;
|
||||||
|
let free_tier_near_capped =
|
||||||
|
free_tier_current as f32 / 100.0 >= free_tier_cap as f32 / 100.0 * 0.9;
|
||||||
|
let over_tier_capped = over_tier_current == over_tier_cap;
|
||||||
|
let over_tier_near_capped =
|
||||||
|
over_tier_current as f32 / 100.0 >= over_tier_cap as f32 / 100.0 * 0.9;
|
||||||
|
|
||||||
|
let free_tier_progress = cx.new(|cx| {
|
||||||
|
ProgressBar::new(
|
||||||
|
"free_tier",
|
||||||
|
free_tier_current as f32,
|
||||||
|
free_tier_cap as f32,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let over_tier_progress = cx.new(|cx| {
|
||||||
|
ProgressBar::new(
|
||||||
|
"over_tier",
|
||||||
|
over_tier_current as f32,
|
||||||
|
over_tier_cap as f32,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if free_tier_capped {
|
||||||
|
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||||
|
progress_bar.fg_color(cx.theme().status().error);
|
||||||
|
});
|
||||||
|
} else if free_tier_near_capped {
|
||||||
|
free_tier_progress.update(cx, |progress_bar, cx| {
|
||||||
|
progress_bar.fg_color(cx.theme().status().warning);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if over_tier_capped {
|
||||||
|
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||||
|
progress_bar.fg_color(cx.theme().status().error);
|
||||||
|
});
|
||||||
|
} else if over_tier_near_capped {
|
||||||
|
over_tier_progress.update(cx, |progress_bar, cx| {
|
||||||
|
progress_bar.fg_color(cx.theme().status().warning);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
free_tier_current,
|
||||||
|
free_tier_cap,
|
||||||
|
over_tier_current,
|
||||||
|
over_tier_cap,
|
||||||
|
free_tier_progress,
|
||||||
|
over_tier_progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for UserSpending {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let formatted_free_tier = format!(
|
||||||
|
"${} / ${}",
|
||||||
|
self.free_tier_current as f32 / 100.0,
|
||||||
|
self.free_tier_cap as f32 / 100.0
|
||||||
|
);
|
||||||
|
let formatted_over_tier = format!(
|
||||||
|
"${} / ${}",
|
||||||
|
self.over_tier_current as f32 / 100.0,
|
||||||
|
self.over_tier_cap as f32 / 100.0
|
||||||
|
);
|
||||||
|
|
||||||
|
v_group()
|
||||||
|
.elevation_2(cx)
|
||||||
|
.py_1p5()
|
||||||
|
.px_2p5()
|
||||||
|
.w(px(360.))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.p_1p5()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(Label::new("Free Tier Usage").size(LabelSize::Small))
|
||||||
|
.child(
|
||||||
|
Label::new(formatted_free_tier)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(self.free_tier_progress.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.p_1p5()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(Label::new("Current Spending").size(LabelSize::Small))
|
||||||
|
.child(
|
||||||
|
Label::new(formatted_over_tier)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(self.over_tier_progress.clone()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for UserSpending {
|
||||||
|
fn scope() -> ComponentScope {
|
||||||
|
ComponentScope::None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||||
|
let new_user = cx.new(|cx| UserSpending::new(0, 2000, 0, 2000, cx));
|
||||||
|
let free_capped = cx.new(|cx| UserSpending::new(2000, 2000, 0, 2000, cx));
|
||||||
|
let free_near_capped = cx.new(|cx| UserSpending::new(1800, 2000, 0, 2000, cx));
|
||||||
|
let over_near_capped = cx.new(|cx| UserSpending::new(2000, 2000, 1800, 2000, cx));
|
||||||
|
let over_capped = cx.new(|cx| UserSpending::new(1000, 2000, 2000, 2000, cx));
|
||||||
|
|
||||||
|
Some(
|
||||||
|
v_flex()
|
||||||
|
.gap_6()
|
||||||
|
.p_4()
|
||||||
|
.children(vec![example_group(vec![
|
||||||
|
single_example(
|
||||||
|
"New User",
|
||||||
|
div().size_full().child(new_user.clone()).into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Free Tier Capped",
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(free_capped.clone())
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Free Tier Near Capped",
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(free_near_capped.clone())
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Over Tier Near Capped",
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(over_near_capped.clone())
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
single_example(
|
||||||
|
"Over Tier Capped",
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.child(over_capped.clone())
|
||||||
|
.into_any_element(),
|
||||||
|
),
|
||||||
|
])])
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -187,22 +187,20 @@ impl ComponentPreview {
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
let known_scopes = [
|
|
||||||
ComponentScope::Layout,
|
|
||||||
ComponentScope::Input,
|
|
||||||
ComponentScope::Editor,
|
|
||||||
ComponentScope::Notification,
|
|
||||||
ComponentScope::Collaboration,
|
|
||||||
ComponentScope::VersionControl,
|
|
||||||
ComponentScope::None,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Always show all components first
|
// Always show all components first
|
||||||
entries.push(PreviewEntry::AllComponents);
|
entries.push(PreviewEntry::AllComponents);
|
||||||
entries.push(PreviewEntry::Separator);
|
entries.push(PreviewEntry::Separator);
|
||||||
|
|
||||||
for scope in known_scopes.iter() {
|
let mut scopes: Vec<_> = scope_groups
|
||||||
if let Some(components) = scope_groups.remove(scope) {
|
.keys()
|
||||||
|
.filter(|scope| !matches!(**scope, ComponentScope::None))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
scopes.sort_by_key(|s| s.to_string());
|
||||||
|
|
||||||
|
for scope in scopes {
|
||||||
|
if let Some(components) = scope_groups.remove(&scope) {
|
||||||
if !components.is_empty() {
|
if !components.is_empty() {
|
||||||
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
|
entries.push(PreviewEntry::SectionHeader(scope.to_string().into()));
|
||||||
let mut sorted_components = components;
|
let mut sorted_components = components;
|
||||||
|
@ -215,6 +213,7 @@ impl ComponentPreview {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add uncategorized components last
|
||||||
if let Some(components) = scope_groups.get(&ComponentScope::None) {
|
if let Some(components) = scope_groups.get(&ComponentScope::None) {
|
||||||
if !components.is_empty() {
|
if !components.is_empty() {
|
||||||
entries.push(PreviewEntry::Separator);
|
entries.push(PreviewEntry::Separator);
|
||||||
|
@ -272,7 +271,12 @@ impl ComponentPreview {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
PreviewEntry::Separator => ListItem::new(ix)
|
PreviewEntry::Separator => ListItem::new(ix)
|
||||||
.child(h_flex().pt_3().child(Divider::horizontal_dashed()))
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.occlude()
|
||||||
|
.pt_3()
|
||||||
|
.child(Divider::horizontal_dashed()),
|
||||||
|
)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ mod notification;
|
||||||
mod numeric_stepper;
|
mod numeric_stepper;
|
||||||
mod popover;
|
mod popover;
|
||||||
mod popover_menu;
|
mod popover_menu;
|
||||||
|
mod progress;
|
||||||
mod radio;
|
mod radio;
|
||||||
mod right_click_menu;
|
mod right_click_menu;
|
||||||
mod scrollbar;
|
mod scrollbar;
|
||||||
|
@ -61,6 +62,7 @@ pub use notification::*;
|
||||||
pub use numeric_stepper::*;
|
pub use numeric_stepper::*;
|
||||||
pub use popover::*;
|
pub use popover::*;
|
||||||
pub use popover_menu::*;
|
pub use popover_menu::*;
|
||||||
|
pub use progress::*;
|
||||||
pub use radio::*;
|
pub use radio::*;
|
||||||
pub use right_click_menu::*;
|
pub use right_click_menu::*;
|
||||||
pub use scrollbar::*;
|
pub use scrollbar::*;
|
||||||
|
|
|
@ -267,7 +267,7 @@ impl RenderOnce for IconWithIndicator {
|
||||||
|
|
||||||
impl Component for Icon {
|
impl Component for Icon {
|
||||||
fn scope() -> ComponentScope {
|
fn scope() -> ComponentScope {
|
||||||
ComponentScope::None
|
ComponentScope::Images
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description() -> Option<&'static str> {
|
fn description() -> Option<&'static str> {
|
||||||
|
|
|
@ -26,7 +26,7 @@ impl RenderOnce for DecoratedIcon {
|
||||||
|
|
||||||
impl Component for DecoratedIcon {
|
impl Component for DecoratedIcon {
|
||||||
fn scope() -> ComponentScope {
|
fn scope() -> ComponentScope {
|
||||||
ComponentScope::None
|
ComponentScope::Images
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description() -> Option<&'static str> {
|
fn description() -> Option<&'static str> {
|
||||||
|
|
|
@ -199,7 +199,7 @@ impl RenderOnce for Label {
|
||||||
|
|
||||||
impl Component for Label {
|
impl Component for Label {
|
||||||
fn scope() -> ComponentScope {
|
fn scope() -> ComponentScope {
|
||||||
ComponentScope::None
|
ComponentScope::Typography
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description() -> Option<&'static str> {
|
fn description() -> Option<&'static str> {
|
||||||
|
|
2
crates/ui/src/components/progress.rs
Normal file
2
crates/ui/src/components/progress.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
mod progress_bar;
|
||||||
|
pub use progress_bar::*;
|
159
crates/ui/src/components/progress/progress_bar.rs
Normal file
159
crates/ui/src/components/progress/progress_bar.rs
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
use documented::Documented;
|
||||||
|
use gpui::{Hsla, point};
|
||||||
|
|
||||||
|
use crate::components::Label;
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// A progress bar is a horizontal bar that communicates the status of a process.
|
||||||
|
///
|
||||||
|
/// A progress bar should not be used to represent indeterminate progress.
|
||||||
|
#[derive(RegisterComponent, Documented)]
|
||||||
|
pub struct ProgressBar {
|
||||||
|
id: ElementId,
|
||||||
|
value: f32,
|
||||||
|
max_value: f32,
|
||||||
|
bg_color: Hsla,
|
||||||
|
fg_color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBar {
|
||||||
|
/// Create a new progress bar with the given value and maximum value.
|
||||||
|
pub fn new(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
value: f32,
|
||||||
|
max_value: f32,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
value,
|
||||||
|
max_value,
|
||||||
|
bg_color: cx.theme().colors().background,
|
||||||
|
fg_color: cx.theme().status().info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the current value of the progress bar.
|
||||||
|
pub fn value(&mut self, value: f32) -> &mut Self {
|
||||||
|
self.value = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the maximum value of the progress bar.
|
||||||
|
pub fn max_value(&mut self, max_value: f32) -> &mut Self {
|
||||||
|
self.max_value = max_value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the background color of the progress bar.
|
||||||
|
pub fn bg_color(&mut self, color: Hsla) -> &mut Self {
|
||||||
|
self.bg_color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the foreground color of the progress bar.
|
||||||
|
pub fn fg_color(&mut self, color: Hsla) -> &mut Self {
|
||||||
|
self.fg_color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ProgressBar {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let fill_width = (self.value / self.max_value).clamp(0.02, 1.0);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(self.id.clone())
|
||||||
|
.w_full()
|
||||||
|
.h(px(8.0))
|
||||||
|
.rounded_full()
|
||||||
|
.py(px(2.0))
|
||||||
|
.px(px(4.0))
|
||||||
|
.bg(self.bg_color)
|
||||||
|
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||||
|
color: gpui::black().opacity(0.08),
|
||||||
|
offset: point(px(0.), px(1.)),
|
||||||
|
blur_radius: px(0.),
|
||||||
|
spread_radius: px(0.),
|
||||||
|
}])
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.h_full()
|
||||||
|
.rounded_full()
|
||||||
|
.bg(self.fg_color)
|
||||||
|
.w(relative(fill_width)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ProgressBar {
|
||||||
|
fn scope() -> ComponentScope {
|
||||||
|
ComponentScope::Status
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description() -> Option<&'static str> {
|
||||||
|
Some(Self::DOCS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||||
|
let max_value = 180.0;
|
||||||
|
|
||||||
|
let empty_progress_bar = cx.new(|cx| ProgressBar::new("empty", 0.0, max_value, cx));
|
||||||
|
let partial_progress_bar =
|
||||||
|
cx.new(|cx| ProgressBar::new("partial", max_value * 0.35, max_value, cx));
|
||||||
|
let filled_progress_bar = cx.new(|cx| ProgressBar::new("filled", max_value, max_value, cx));
|
||||||
|
|
||||||
|
Some(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_4()
|
||||||
|
.p_4()
|
||||||
|
.w(px(240.0))
|
||||||
|
.child(div().child("Progress Bar"))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(Label::new("0%"))
|
||||||
|
.child(Label::new("Empty")),
|
||||||
|
)
|
||||||
|
.child(empty_progress_bar.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(Label::new("38%"))
|
||||||
|
.child(Label::new("Partial")),
|
||||||
|
)
|
||||||
|
.child(partial_progress_bar.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(Label::new("100%"))
|
||||||
|
.child(Label::new("Complete")),
|
||||||
|
)
|
||||||
|
.child(filled_progress_bar.clone()),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -235,7 +235,7 @@ impl Headline {
|
||||||
|
|
||||||
impl Component for Headline {
|
impl Component for Headline {
|
||||||
fn scope() -> ComponentScope {
|
fn scope() -> ComponentScope {
|
||||||
ComponentScope::None
|
ComponentScope::Typography
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description() -> Option<&'static str> {
|
fn description() -> Option<&'static str> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue