mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 15:48:11 +08:00
Add the unit, display_decimal_places, and step parameter widget macro attributes (#2706)
* UI working for spacing enum. * Implementations. * UI working for spacing enum. * Undo all changes. * unit, display_decimal_places, and step macro implementation. * Fixing tests. --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
f3720bf6f2
commit
643d7b4432
@@ -121,6 +121,9 @@ pub(crate) fn property_from_type(
|
|||||||
index: usize,
|
index: usize,
|
||||||
ty: &Type,
|
ty: &Type,
|
||||||
number_options: (Option<f64>, Option<f64>, Option<(f64, f64)>),
|
number_options: (Option<f64>, Option<f64>, Option<(f64, f64)>),
|
||||||
|
unit: Option<&str>,
|
||||||
|
display_decimal_places: Option<u32>,
|
||||||
|
step: Option<f64>,
|
||||||
context: &mut NodePropertiesContext,
|
context: &mut NodePropertiesContext,
|
||||||
) -> Result<Vec<LayoutGroup>, Vec<LayoutGroup>> {
|
) -> Result<Vec<LayoutGroup>, Vec<LayoutGroup>> {
|
||||||
let Some(network) = context.network_interface.nested_network(context.selection_network_path) else {
|
let Some(network) = context.network_interface.nested_network(context.selection_network_path) else {
|
||||||
@@ -142,6 +145,15 @@ pub(crate) fn property_from_type(
|
|||||||
number_max = Some(range_end);
|
number_max = Some(range_end);
|
||||||
number_input = number_input.mode_range().min(range_start).max(range_end);
|
number_input = number_input.mode_range().min(range_start).max(range_end);
|
||||||
}
|
}
|
||||||
|
if let Some(unit) = unit {
|
||||||
|
number_input = number_input.unit(unit);
|
||||||
|
}
|
||||||
|
if let Some(display_decimal_places) = display_decimal_places {
|
||||||
|
number_input = number_input.display_decimal_places(display_decimal_places);
|
||||||
|
}
|
||||||
|
if let Some(step) = step {
|
||||||
|
number_input = number_input.step(step);
|
||||||
|
}
|
||||||
|
|
||||||
let min = |x: f64| number_min.unwrap_or(x);
|
let min = |x: f64| number_min.unwrap_or(x);
|
||||||
let max = |x: f64| number_max.unwrap_or(x);
|
let max = |x: f64| number_max.unwrap_or(x);
|
||||||
@@ -155,15 +167,15 @@ pub(crate) fn property_from_type(
|
|||||||
// Aliased types (ambiguous values)
|
// Aliased types (ambiguous values)
|
||||||
Some("Percentage") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
|
Some("Percentage") => number_widget(default_info, number_input.percentage().min(min(0.)).max(max(100.))).into(),
|
||||||
Some("SignedPercentage") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
|
Some("SignedPercentage") => number_widget(default_info, number_input.percentage().min(min(-100.)).max(max(100.))).into(),
|
||||||
Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit("°")).into(),
|
Some("Angle") => number_widget(default_info, number_input.mode_range().min(min(-180.)).max(max(180.)).unit(unit.unwrap_or("°"))).into(),
|
||||||
Some("Multiplier") => number_widget(default_info, number_input.unit("x")).into(),
|
Some("Multiplier") => number_widget(default_info, number_input.unit(unit.unwrap_or("x"))).into(),
|
||||||
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(" px")).into(),
|
Some("PixelLength") => number_widget(default_info, number_input.min(min(0.)).unit(unit.unwrap_or(" px"))).into(),
|
||||||
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
|
Some("Length") => number_widget(default_info, number_input.min(min(0.))).into(),
|
||||||
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
|
Some("Fraction") => number_widget(default_info, number_input.mode_range().min(min(0.)).max(max(1.))).into(),
|
||||||
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
|
Some("IntegerCount") => number_widget(default_info, number_input.int().min(min(1.))).into(),
|
||||||
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
Some("SeedValue") => number_widget(default_info, number_input.int().min(min(0.))).into(),
|
||||||
Some("Resolution") => coordinate_widget(default_info, "W", "H", " px", Some(64.)),
|
Some("Resolution") => coordinate_widget(default_info, "W", "H", unit.unwrap_or(" px"), Some(64.)),
|
||||||
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", " px", None),
|
Some("PixelSize") => coordinate_widget(default_info, "X", "Y", unit.unwrap_or(" px"), None),
|
||||||
|
|
||||||
// For all other types, use TypeId-based matching
|
// For all other types, use TypeId-based matching
|
||||||
_ => {
|
_ => {
|
||||||
@@ -249,8 +261,8 @@ pub(crate) fn property_from_type(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(),
|
Type::Generic(_) => vec![TextLabel::new("Generic type (not supported)").widget_holder()].into(),
|
||||||
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, context),
|
Type::Fn(_, out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
|
||||||
Type::Future(out) => return property_from_type(node_id, index, out, number_options, context),
|
Type::Future(out) => return property_from_type(node_id, index, out, number_options, unit, display_decimal_places, step, context),
|
||||||
};
|
};
|
||||||
|
|
||||||
extra_widgets.push(widgets);
|
extra_widgets.push(widgets);
|
||||||
@@ -1395,6 +1407,9 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut number_options = (None, None, None);
|
let mut number_options = (None, None, None);
|
||||||
|
let mut display_decimal_places = None;
|
||||||
|
let mut step = None;
|
||||||
|
let mut unit_suffix = None;
|
||||||
let input_type = match implementation {
|
let input_type = match implementation {
|
||||||
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: {
|
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => 'early_return: {
|
||||||
if let Some(field) = graphene_core::registry::NODE_METADATA
|
if let Some(field) = graphene_core::registry::NODE_METADATA
|
||||||
@@ -1404,6 +1419,9 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
|||||||
.and_then(|metadata| metadata.fields.get(input_index))
|
.and_then(|metadata| metadata.fields.get(input_index))
|
||||||
{
|
{
|
||||||
number_options = (field.number_min, field.number_max, field.number_mode_range);
|
number_options = (field.number_min, field.number_max, field.number_mode_range);
|
||||||
|
display_decimal_places = field.number_display_decimal_places;
|
||||||
|
unit_suffix = field.unit;
|
||||||
|
step = field.number_step;
|
||||||
if let Some(ref default) = field.default_type {
|
if let Some(ref default) = field.default_type {
|
||||||
break 'early_return default.clone();
|
break 'early_return default.clone();
|
||||||
}
|
}
|
||||||
@@ -1417,7 +1435,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
|||||||
let mut input_types = implementations
|
let mut input_types = implementations
|
||||||
.keys()
|
.keys()
|
||||||
.filter_map(|item| item.inputs.get(input_index))
|
.filter_map(|item| item.inputs.get(input_index))
|
||||||
.filter(|ty| property_from_type(node_id, input_index, ty, number_options, context).is_ok())
|
.filter(|ty| property_from_type(node_id, input_index, ty, number_options, unit_suffix, display_decimal_places, step, context).is_ok())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
input_types.sort_by_key(|ty| ty.type_name());
|
input_types.sort_by_key(|ty| ty.type_name());
|
||||||
let input_type = input_types.first().cloned();
|
let input_type = input_types.first().cloned();
|
||||||
@@ -1431,7 +1449,7 @@ pub(crate) fn generate_node_properties(node_id: NodeId, context: &mut NodeProper
|
|||||||
_ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0,
|
_ => context.network_interface.input_type(&InputConnector::node(node_id, input_index), context.selection_network_path).0,
|
||||||
};
|
};
|
||||||
|
|
||||||
property_from_type(node_id, input_index, &input_type, number_options, context).unwrap_or_else(|value| value)
|
property_from_type(node_id, input_index, &input_type, number_options, unit_suffix, display_decimal_places, step, context).unwrap_or_else(|value| value)
|
||||||
});
|
});
|
||||||
|
|
||||||
layout.extend(row);
|
layout.extend(row);
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ pub struct FieldMetadata {
|
|||||||
pub number_min: Option<f64>,
|
pub number_min: Option<f64>,
|
||||||
pub number_max: Option<f64>,
|
pub number_max: Option<f64>,
|
||||||
pub number_mode_range: Option<(f64, f64)>,
|
pub number_mode_range: Option<(f64, f64)>,
|
||||||
|
pub number_display_decimal_places: Option<u32>,
|
||||||
|
pub number_step: Option<f64>,
|
||||||
|
pub unit: Option<&'static str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
|
pub trait ChoiceTypeStatic: Sized + Copy + crate::vector::misc::AsU32 + Send + Sync {
|
||||||
|
|||||||
@@ -163,6 +163,41 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||||||
_ => quote!(None),
|
_ => quote!(None),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
let number_display_decimal_places: Vec<_> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| match field {
|
||||||
|
ParsedField::Regular {
|
||||||
|
number_display_decimal_places: Some(decimal_places),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ParsedField::Node {
|
||||||
|
number_display_decimal_places: Some(decimal_places),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
quote!(Some(#decimal_places))
|
||||||
|
}
|
||||||
|
_ => quote!(None),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let number_step: Vec<_> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| match field {
|
||||||
|
ParsedField::Regular { number_step: Some(step), .. } | ParsedField::Node { number_step: Some(step), .. } => {
|
||||||
|
quote!(Some(#step))
|
||||||
|
}
|
||||||
|
_ => quote!(None),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let unit_suffix: Vec<_> = fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| match field {
|
||||||
|
ParsedField::Regular { unit: Some(unit), .. } | ParsedField::Node { unit: Some(unit), .. } => {
|
||||||
|
quote!(Some(#unit))
|
||||||
|
}
|
||||||
|
_ => quote!(None),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let exposed: Vec<_> = fields
|
let exposed: Vec<_> = fields
|
||||||
.iter()
|
.iter()
|
||||||
@@ -375,6 +410,9 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
|
|||||||
number_min: #number_min_values,
|
number_min: #number_min_values,
|
||||||
number_max: #number_max_values,
|
number_max: #number_max_values,
|
||||||
number_mode_range: #number_mode_range_values,
|
number_mode_range: #number_mode_range_values,
|
||||||
|
number_display_decimal_places: #number_display_decimal_places,
|
||||||
|
number_step: #number_step,
|
||||||
|
unit: #unit_suffix,
|
||||||
},
|
},
|
||||||
)*
|
)*
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use syn::punctuated::Punctuated;
|
|||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
use syn::token::{Comma, RArrow};
|
use syn::token::{Comma, RArrow};
|
||||||
use syn::{
|
use syn::{
|
||||||
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause, parse_quote,
|
AttrStyle, Attribute, Error, Expr, ExprTuple, FnArg, GenericParam, Ident, ItemFn, Lit, LitFloat, LitInt, LitStr, Meta, Pat, PatIdent, PatType, Path, ReturnType, Type, TypeParam, WhereClause,
|
||||||
|
parse_quote,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::codegen::generate_node_code;
|
use crate::codegen::generate_node_code;
|
||||||
@@ -110,7 +111,10 @@ pub(crate) enum ParsedField {
|
|||||||
number_hard_min: Option<LitFloat>,
|
number_hard_min: Option<LitFloat>,
|
||||||
number_hard_max: Option<LitFloat>,
|
number_hard_max: Option<LitFloat>,
|
||||||
number_mode_range: Option<ExprTuple>,
|
number_mode_range: Option<ExprTuple>,
|
||||||
|
number_display_decimal_places: Option<LitInt>,
|
||||||
|
number_step: Option<LitFloat>,
|
||||||
implementations: Punctuated<Type, Comma>,
|
implementations: Punctuated<Type, Comma>,
|
||||||
|
unit: Option<LitStr>,
|
||||||
},
|
},
|
||||||
Node {
|
Node {
|
||||||
pat_ident: PatIdent,
|
pat_ident: PatIdent,
|
||||||
@@ -119,7 +123,10 @@ pub(crate) enum ParsedField {
|
|||||||
widget_override: ParsedWidgetOverride,
|
widget_override: ParsedWidgetOverride,
|
||||||
input_type: Type,
|
input_type: Type,
|
||||||
output_type: Type,
|
output_type: Type,
|
||||||
|
number_display_decimal_places: Option<LitInt>,
|
||||||
|
number_step: Option<LitFloat>,
|
||||||
implementations: Punctuated<Implementation, Comma>,
|
implementations: Punctuated<Implementation, Comma>,
|
||||||
|
unit: Option<LitStr>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -466,6 +473,35 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let unit = extract_attribute(attrs, "unit")
|
||||||
|
.map(|attr| attr.parse_args::<LitStr>().map_err(|e| Error::new_spanned(attr, format!("Expected a unit type as string"))))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let number_display_decimal_places = extract_attribute(attrs, "display_decimal_places")
|
||||||
|
.map(|attr| {
|
||||||
|
attr.parse_args::<LitInt>().map_err(|e| {
|
||||||
|
Error::new_spanned(
|
||||||
|
attr,
|
||||||
|
format!("Invalid `integer` for number of decimals for argument '{}': {}\nUSAGE EXAMPLE: #[display_decimal_places(2)]", ident, e),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()?
|
||||||
|
.map(|f| {
|
||||||
|
if let Err(e) = f.base10_parse::<u32>() {
|
||||||
|
Err(Error::new_spanned(f, format!("Expected a `u32` for `display_decimal_places` for '{}': {}", ident, e)))
|
||||||
|
} else {
|
||||||
|
Ok(f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
let number_step = extract_attribute(attrs, "step")
|
||||||
|
.map(|attr| {
|
||||||
|
attr.parse_args::<LitFloat>()
|
||||||
|
.map_err(|e| Error::new_spanned(attr, format!("Invalid `step` for argument '{}': {}\nUSAGE EXAMPLE: #[step(2.)]", ident, e)))
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
let (is_node, node_input_type, node_output_type) = parse_node_type(&ty);
|
let (is_node, node_input_type, node_output_type) = parse_node_type(&ty);
|
||||||
let description = attrs
|
let description = attrs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -502,7 +538,10 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||||||
widget_override,
|
widget_override,
|
||||||
input_type,
|
input_type,
|
||||||
output_type,
|
output_type,
|
||||||
|
number_display_decimal_places,
|
||||||
|
number_step,
|
||||||
implementations,
|
implementations,
|
||||||
|
unit,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let implementations = extract_attribute(attrs, "implementations")
|
let implementations = extract_attribute(attrs, "implementations")
|
||||||
@@ -520,9 +559,12 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
|
|||||||
number_hard_min,
|
number_hard_min,
|
||||||
number_hard_max,
|
number_hard_max,
|
||||||
number_mode_range,
|
number_mode_range,
|
||||||
|
number_display_decimal_places,
|
||||||
|
number_step,
|
||||||
ty,
|
ty,
|
||||||
value_source,
|
value_source,
|
||||||
implementations,
|
implementations,
|
||||||
|
unit,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -738,7 +780,10 @@ mod tests {
|
|||||||
number_hard_min: None,
|
number_hard_min: None,
|
||||||
number_hard_max: None,
|
number_hard_max: None,
|
||||||
number_mode_range: None,
|
number_mode_range: None,
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
|
unit: None,
|
||||||
}],
|
}],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
crate_name: FoundCrate::Itself,
|
crate_name: FoundCrate::Itself,
|
||||||
@@ -790,7 +835,10 @@ mod tests {
|
|||||||
widget_override: ParsedWidgetOverride::None,
|
widget_override: ParsedWidgetOverride::None,
|
||||||
input_type: parse_quote!(Footprint),
|
input_type: parse_quote!(Footprint),
|
||||||
output_type: parse_quote!(T),
|
output_type: parse_quote!(T),
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
|
unit: None,
|
||||||
},
|
},
|
||||||
ParsedField::Regular {
|
ParsedField::Regular {
|
||||||
pat_ident: pat_ident("translate"),
|
pat_ident: pat_ident("translate"),
|
||||||
@@ -805,7 +853,10 @@ mod tests {
|
|||||||
number_hard_min: None,
|
number_hard_min: None,
|
||||||
number_hard_max: None,
|
number_hard_max: None,
|
||||||
number_mode_range: None,
|
number_mode_range: None,
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
|
unit: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
@@ -860,7 +911,10 @@ mod tests {
|
|||||||
number_hard_min: None,
|
number_hard_min: None,
|
||||||
number_hard_max: None,
|
number_hard_max: None,
|
||||||
number_mode_range: None,
|
number_mode_range: None,
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
|
unit: None,
|
||||||
}],
|
}],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
crate_name: FoundCrate::Itself,
|
crate_name: FoundCrate::Itself,
|
||||||
@@ -913,12 +967,15 @@ mod tests {
|
|||||||
number_hard_min: None,
|
number_hard_min: None,
|
||||||
number_hard_max: None,
|
number_hard_max: None,
|
||||||
number_mode_range: None,
|
number_mode_range: None,
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: {
|
implementations: {
|
||||||
let mut p = Punctuated::new();
|
let mut p = Punctuated::new();
|
||||||
p.push(parse_quote!(f32));
|
p.push(parse_quote!(f32));
|
||||||
p.push(parse_quote!(f64));
|
p.push(parse_quote!(f64));
|
||||||
p
|
p
|
||||||
},
|
},
|
||||||
|
unit: None,
|
||||||
}],
|
}],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
crate_name: FoundCrate::Itself,
|
crate_name: FoundCrate::Itself,
|
||||||
@@ -978,7 +1035,10 @@ mod tests {
|
|||||||
number_hard_min: None,
|
number_hard_min: None,
|
||||||
number_hard_max: None,
|
number_hard_max: None,
|
||||||
number_mode_range: Some(parse_quote!((0., 100.))),
|
number_mode_range: Some(parse_quote!((0., 100.))),
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
|
unit: None,
|
||||||
}],
|
}],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
crate_name: FoundCrate::Itself,
|
crate_name: FoundCrate::Itself,
|
||||||
@@ -1031,7 +1091,10 @@ mod tests {
|
|||||||
number_hard_min: None,
|
number_hard_min: None,
|
||||||
number_hard_max: None,
|
number_hard_max: None,
|
||||||
number_mode_range: None,
|
number_mode_range: None,
|
||||||
|
number_display_decimal_places: None,
|
||||||
|
number_step: None,
|
||||||
implementations: Punctuated::new(),
|
implementations: Punctuated::new(),
|
||||||
|
unit: None,
|
||||||
}],
|
}],
|
||||||
body: TokenStream2::new(),
|
body: TokenStream2::new(),
|
||||||
crate_name: FoundCrate::Itself,
|
crate_name: FoundCrate::Itself,
|
||||||
|
|||||||
Reference in New Issue
Block a user