Skip to content

Commit 23a618f

Browse files
committed
Add groups export.
1 parent e1073bc commit 23a618f

File tree

9 files changed

+417
-44
lines changed

9 files changed

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

0 commit comments

Comments
 (0)