Skip to content

Commit 8a6f306

Browse files
committed
Add groups export - gdscript-like implementation.
1 parent d104749 commit 8a6f306

File tree

10 files changed

+150
-308
lines changed

10 files changed

+150
-308
lines changed

godot-core/src/registry/godot_register_wrappers.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ fn register_var_or_export_inner(
8080
}
8181
}
8282

83-
pub fn register_group<C: GodotClass>(group_name: &str) {
83+
pub fn register_group<C: GodotClass>(group_name: &str, prefix: &str) {
8484
let group_name = GString::from(group_name);
85-
let prefix = GString::default();
85+
let prefix = GString::from(prefix);
8686
let class_name = C::class_name();
8787

8888
unsafe {
@@ -95,9 +95,9 @@ pub fn register_group<C: GodotClass>(group_name: &str) {
9595
}
9696
}
9797

98-
pub fn register_subgroup<C: GodotClass>(subgroup_name: &str) {
98+
pub fn register_subgroup<C: GodotClass>(subgroup_name: &str, prefix: &str) {
9999
let subgroup_name = GString::from(subgroup_name);
100-
let prefix = GString::default();
100+
let prefix = GString::from(prefix);
101101
let class_name = C::class_name();
102102

103103
unsafe {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct Field {
1818
pub var: Option<FieldVar>,
1919
pub export: Option<FieldExport>,
2020
pub group: Option<FieldGroup>,
21+
pub subgroup: Option<FieldGroup>,
2122
pub is_onready: bool,
2223
pub is_oneditor: bool,
2324
#[cfg(feature = "register-docs")]
@@ -34,6 +35,7 @@ impl Field {
3435
var: None,
3536
export: None,
3637
group: None,
38+
subgroup: None,
3739
is_onready: false,
3840
is_oneditor: false,
3941
#[cfg(feature = "register-docs")]

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@ use proc_macro2::{Punct, TokenStream};
1212
use std::fmt::Display;
1313

1414
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-
2115
/// All fields except `base_field`.
2216
pub all_fields: Vec<Field>,
2317

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

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

8-
use crate::class::data_models::fields::Fields;
9-
use crate::util::{bail, KvParser};
10-
use crate::ParseResult;
11-
use std::cmp::Ordering;
8+
// Note: group membership for properties in Godot is based on the order of their registration.
9+
// All the properties belong to group or subgroup registered beforehand, identically as in gdscript.
10+
// Initial implementation providing clap-like API with an explicit sorting
11+
// & groups/subgroups declared for each field (`#[export(group = ..., subgroup = ...)]`
12+
// can be found at: https://github.com/godot-rust/gdext/pull/1214.
1213

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;
14+
use crate::util::KvParser;
15+
use crate::ParseResult;
16+
use proc_macro2::Literal;
1717

18+
/// Specifies group or subgroup which starts with a given field.
19+
/// Group membership for properties in Godot is based on the order of their registration –
20+
/// i.e. given field belongs to group declared beforehand (for example with some previous field).
1821
pub struct FieldGroup {
19-
pub group_name_index: Option<GroupIdentifier>,
20-
pub subgroup_name_index: Option<GroupIdentifier>,
22+
pub(crate) name: Literal,
23+
pub(crate) prefix: Literal,
2124
}
2225

2326
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)?,
27+
pub(crate) fn new_from_kv(parser: &mut KvParser) -> ParseResult<Self> {
28+
// Groups without name and prefix are totally valid in godot and used
29+
// to "break" out of groups and subgroups. See:
30+
// https://docs.godotengine.org/en/4.4/classes/[email protected]#class-gdscript-annotation-export-group
31+
let (name, prefix) = (
32+
parser
33+
.handle_literal("name", "String")?
34+
.unwrap_or(Literal::string("")),
35+
parser
36+
.handle_literal("prefix", "String")?
37+
.unwrap_or(Literal::string("")),
5138
);
5239

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-
})
40+
Ok(Self { name, prefix })
6441
}
6542
}
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)