Skip to content

Commit e39f08a

Browse files
committed
Add groups export – clap-like implementation.
1 parent 66e9a01 commit e39f08a

File tree

11 files changed

+486
-56
lines changed

11 files changed

+486
-56
lines changed

godot-core/src/registry/godot_register_wrappers.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
//! Internal registration machinery used by proc-macro APIs.
99
10-
use crate::builtin::StringName;
10+
use crate::builtin::{GString, StringName};
1111
use crate::global::PropertyUsageFlags;
1212
use crate::meta::{ClassName, GodotConvert, GodotType, PropertyHintInfo, PropertyInfo};
1313
use crate::obj::GodotClass;
@@ -79,3 +79,33 @@ fn register_var_or_export_inner(
7979
);
8080
}
8181
}
82+
83+
pub fn register_group<C: GodotClass>(group_name: &str) {
84+
let group_name = GString::from(group_name);
85+
let prefix = GString::default();
86+
let class_name = C::class_name();
87+
88+
unsafe {
89+
sys::interface_fn!(classdb_register_extension_class_property_group)(
90+
sys::get_library(),
91+
class_name.string_sys(),
92+
group_name.string_sys(),
93+
prefix.string_sys(),
94+
);
95+
}
96+
}
97+
98+
pub fn register_subgroup<C: GodotClass>(subgroup_name: &str) {
99+
let subgroup_name = GString::from(subgroup_name);
100+
let prefix = GString::default();
101+
let class_name = C::class_name();
102+
103+
unsafe {
104+
sys::interface_fn!(classdb_register_extension_class_property_subgroup)(
105+
sys::get_library(),
106+
class_name.string_sys(),
107+
subgroup_name.string_sys(),
108+
prefix.string_sys(),
109+
);
110+
}
111+
}

godot-macros/src/class/data_models/field.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8+
use crate::class::data_models::group_export::FieldGroup;
89
use crate::class::{FieldExport, FieldVar};
910
use crate::util::{error, KvParser};
1011
use proc_macro2::{Ident, Span, TokenStream};
@@ -16,6 +17,7 @@ pub struct Field {
1617
pub default_val: Option<FieldDefault>,
1718
pub var: Option<FieldVar>,
1819
pub export: Option<FieldExport>,
20+
pub group: Option<FieldGroup>,
1921
pub is_onready: bool,
2022
pub is_oneditor: bool,
2123
#[cfg(feature = "register-docs")]
@@ -31,6 +33,7 @@ impl Field {
3133
default_val: None,
3234
var: None,
3335
export: None,
36+
group: None,
3437
is_onready: false,
3538
is_oneditor: false,
3639
#[cfg(feature = "register-docs")]
@@ -110,20 +113,6 @@ pub enum FieldCond {
110113
IsOnEditor,
111114
}
112115

113-
pub struct Fields {
114-
/// All fields except `base_field`.
115-
pub all_fields: Vec<Field>,
116-
117-
/// The field with type `Base<T>`, if available.
118-
pub base_field: Option<Field>,
119-
120-
/// Deprecation warnings.
121-
pub deprecations: Vec<TokenStream>,
122-
123-
/// Errors during macro evaluation that shouldn't abort the execution of the macro.
124-
pub errors: Vec<venial::Error>,
125-
}
126-
127116
#[derive(Clone)]
128117
pub struct FieldDefault {
129118
pub default_val: TokenStream,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use crate::class::Field;
9+
use crate::util::bail;
10+
use crate::ParseResult;
11+
use proc_macro2::{Punct, TokenStream};
12+
use std::fmt::Display;
13+
14+
pub struct Fields {
15+
/// Names of all the declared groups and subgroups for this struct.
16+
// In the future might be split in two (for groups and subgroups) & used to define the priority (order) of said groups.
17+
// Currently order of declaration declares the group priority (i.e. – groups declared first are shown as the first in the editor).
18+
// This order is not guaranteed but so far proved to work reliably.
19+
pub groups: Vec<String>,
20+
21+
/// All fields except `base_field`.
22+
pub all_fields: Vec<Field>,
23+
24+
/// The field with type `Base<T>`, if available.
25+
pub base_field: Option<Field>,
26+
27+
/// Deprecation warnings.
28+
pub deprecations: Vec<TokenStream>,
29+
30+
/// Errors during macro evaluation that shouldn't abort the execution of the macro.
31+
pub errors: Vec<venial::Error>,
32+
}
33+
34+
/// Fetches data for all named fields for a struct.
35+
///
36+
/// Errors if `class` is a tuple struct.
37+
pub fn named_fields(
38+
class: &venial::Struct,
39+
derive_macro_name: impl Display,
40+
) -> ParseResult<Vec<(venial::NamedField, Punct)>> {
41+
// This is separate from parse_fields to improve compile errors. The errors from here demand larger and more non-local changes from the API
42+
// user than those from parse_struct_attributes, so this must be run first.
43+
match &class.fields {
44+
// TODO disallow unit structs in the future
45+
// It often happens that over time, a registered class starts to require a base field.
46+
// Extending a {} struct requires breaking less code, so we should encourage it from the start.
47+
venial::Fields::Unit => Ok(vec![]),
48+
venial::Fields::Tuple(_) => bail!(
49+
&class.fields,
50+
"{derive_macro_name} is not supported for tuple structs",
51+
)?,
52+
venial::Fields::Named(fields) => Ok(fields.fields.inner.clone()),
53+
}
54+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright (c) godot-rust; Bromeon and contributors.
3+
* This Source Code Form is subject to the terms of the Mozilla Public
4+
* License, v. 2.0. If a copy of the MPL was not distributed with this
5+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
*/
7+
8+
use crate::class::data_models::fields::Fields;
9+
use crate::util::{bail, KvParser};
10+
use crate::ParseResult;
11+
use std::cmp::Ordering;
12+
13+
/// Points to index of a given group name in [Fields.groups](field@Fields::groups).
14+
///
15+
/// Two fields with the same GroupIdentifier belong to the same group.
16+
pub type GroupIdentifier = usize;
17+
18+
pub struct FieldGroup {
19+
pub group_name_index: Option<GroupIdentifier>,
20+
pub subgroup_name_index: Option<GroupIdentifier>,
21+
}
22+
23+
impl FieldGroup {
24+
fn parse_group(
25+
expr: &'static str,
26+
parser: &mut KvParser,
27+
groups: &mut Vec<String>,
28+
) -> ParseResult<Option<GroupIdentifier>> {
29+
let Some(group) = parser.handle_string(expr)? else {
30+
return Ok(None);
31+
};
32+
33+
if let Some(group_index) = groups
34+
.iter()
35+
.position(|existing_group| existing_group == &group)
36+
{
37+
Ok(Some(group_index))
38+
} else {
39+
groups.push(group);
40+
Ok(Some(groups.len() - 1))
41+
}
42+
}
43+
44+
pub(crate) fn new_from_kv(
45+
parser: &mut KvParser,
46+
groups: &mut Vec<String>,
47+
) -> ParseResult<Self> {
48+
let (group_name_index, subgroup_name_index) = (
49+
Self::parse_group("group", parser, groups)?,
50+
Self::parse_group("subgroup", parser, groups)?,
51+
);
52+
53+
// Declaring only a subgroup for given property – with no group at all – is totally valid in Godot.
54+
// Unfortunately it leads to a lot of very janky and not too ideal behaviours
55+
// So it is better to treat it as a user error.
56+
if subgroup_name_index.is_some() && group_name_index.is_none() {
57+
return bail!(parser.span(), "Subgroups without groups are not supported.");
58+
}
59+
60+
Ok(Self {
61+
group_name_index,
62+
subgroup_name_index,
63+
})
64+
}
65+
}
66+
67+
/// Remove surrounding quotes to display declared "group name" in editor as `group name` instead of `"group name"`.
68+
/// Should be called after parsing all the fields to avoid unnecessary operations.
69+
pub(crate) fn format_groups(groups: Vec<String>) -> Vec<String> {
70+
groups
71+
.into_iter()
72+
.map(|g| g.trim_matches('"').to_string())
73+
.collect()
74+
}
75+
76+
// ----------------------------------------------------------------------------------------------------------------------------------------------
77+
// Ordering
78+
79+
pub(crate) struct ExportGroupOrdering {
80+
/// Allows to identify given export group.
81+
/// `None` for root.
82+
identifier: Option<GroupIdentifier>,
83+
/// Contains subgroups of given ordering (subgroups for groups, subgroups&groups for root).
84+
/// Ones parsed first have higher priority, i.e. are displayed as the first.
85+
subgroups: Vec<ExportGroupOrdering>,
86+
}
87+
88+
impl ExportGroupOrdering {
89+
/// Creates root which holds all the groups&subgroups.
90+
/// Should be called only once in a given context.
91+
fn root() -> Self {
92+
Self {
93+
identifier: None,
94+
subgroups: Vec::new(),
95+
}
96+
}
97+
98+
/// Represents individual group & its subgroups.
99+
fn child(identifier: GroupIdentifier) -> Self {
100+
Self {
101+
identifier: Some(identifier),
102+
subgroups: Vec::new(),
103+
}
104+
}
105+
106+
/// Returns registered group index. Registers given group if not present.
107+
fn group_index(&mut self, identifier: &GroupIdentifier) -> usize {
108+
self.subgroups
109+
.iter()
110+
// Will never fail – non-root orderings must have an identifier.
111+
.position(|sub| identifier == sub.identifier.as_ref().expect("Tried to parse an undefined export group. This is a bug, please report it."))
112+
.unwrap_or_else(|| {
113+
// Register new subgroup.
114+
self.subgroups.push(ExportGroupOrdering::child(*identifier));
115+
self.subgroups.len() - 1
116+
})
117+
}
118+
}
119+
120+
// Note: GDExtension doesn't support categories for some reason(s?).
121+
// It probably expects us to use inheritance instead?
122+
enum OrderingStage {
123+
Group,
124+
SubGroup,
125+
}
126+
127+
// It is recursive but max recursion depth is 2 (root -> group -> subgroup) so it's fine.
128+
fn compare_by_group_and_declaration_order(
129+
field_a: &FieldGroup,
130+
field_b: &FieldGroup,
131+
ordering: &mut ExportGroupOrdering,
132+
stage: OrderingStage,
133+
) -> Ordering {
134+
let (lhs, rhs, next_stage) = match stage {
135+
OrderingStage::Group => (
136+
&field_a.group_name_index,
137+
&field_b.group_name_index,
138+
Some(OrderingStage::SubGroup),
139+
),
140+
OrderingStage::SubGroup => (
141+
&field_a.subgroup_name_index,
142+
&field_b.subgroup_name_index,
143+
None,
144+
),
145+
};
146+
147+
match (lhs, rhs) {
148+
// Ungrouped fields or fields with subgroup only always have higher priority (i.e. are displayed on top).
149+
(Some(_), None) => Ordering::Greater,
150+
(None, Some(_)) => Ordering::Less,
151+
152+
// Same group/subgroup.
153+
(Some(group_a), Some(group_b)) => {
154+
if group_a == group_b {
155+
let Some(next_stage) = next_stage else {
156+
return Ordering::Equal;
157+
};
158+
159+
let next_ordering_position = ordering.group_index(group_a);
160+
161+
// Fields belong to the same group – check the subgroup.
162+
compare_by_group_and_declaration_order(
163+
field_a,
164+
field_b,
165+
&mut ordering.subgroups[next_ordering_position],
166+
next_stage,
167+
)
168+
} else {
169+
// Parsed earlier => greater priority.
170+
let (priority_a, priority_b) = (
171+
usize::MAX - ordering.group_index(group_a),
172+
usize::MAX - ordering.group_index(group_b),
173+
);
174+
priority_b.cmp(&priority_a)
175+
}
176+
}
177+
178+
(None, None) => {
179+
// Fields don't belong to any subgroup nor group.
180+
let Some(next_stage) = next_stage else {
181+
return Ordering::Equal;
182+
};
183+
184+
compare_by_group_and_declaration_order(field_a, field_b, ordering, next_stage)
185+
}
186+
}
187+
}
188+
189+
/// Sorts fields by their group and subgroup association.
190+
///
191+
/// Fields without group nor subgroup are first.
192+
/// Fields with subgroup only come in next, in order of their declaration on the class struct.
193+
/// Finally fields with groups are displayed – firstly ones without subgroups followed by
194+
/// fields with given group & subgroup (in the same order as above).
195+
///
196+
/// Group membership for properties in Godot is based on the order of their registration.
197+
/// All the properties belong to group or subgroup registered beforehand – thus the need to sort them.
198+
pub(crate) fn sort_fields_by_group(fields: &mut Fields) {
199+
let mut initial_ordering = ExportGroupOrdering::root();
200+
201+
// `sort_by` instead of `sort_unstable_by` to preserve original order of declaration.
202+
// Which is not guaranteed by the way albeit worked reliably so far.
203+
fields.all_fields.sort_by(|a, b| {
204+
let (group_a, group_b) = match (&a.group, &b.group) {
205+
(Some(a), Some(b)) => (a, b),
206+
(Some(_), None) => return Ordering::Greater,
207+
(None, Some(_)) => return Ordering::Less,
208+
// We don't care about ordering of fields without a `#[export]`.
209+
_ => return Ordering::Equal,
210+
};
211+
212+
compare_by_group_and_declaration_order(
213+
group_a,
214+
group_b,
215+
&mut initial_ordering,
216+
OrderingStage::Group,
217+
)
218+
});
219+
}

0 commit comments

Comments
 (0)