Skip to content

Commit f67a1ae

Browse files
authored
Merge pull request #1161 from godot-rust/feature/string-padding
String formatting: support padding, alignment and precision
2 parents 4a23383 + ada3750 commit f67a1ae

File tree

7 files changed

+162
-7
lines changed

7 files changed

+162
-7
lines changed

godot-core/src/builtin/string/gstring.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use godot_ffi as sys;
1313
use sys::types::OpaqueString;
1414
use sys::{ffi_methods, interface_fn, GodotFfi};
1515

16-
use crate::builtin::string::Encoding;
16+
use crate::builtin::string::{pad_if_needed, Encoding};
1717
use crate::builtin::{inner, NodePath, StringName, Variant};
1818
use crate::meta::error::StringError;
1919
use crate::meta::AsArg;
@@ -298,11 +298,13 @@ impl_shared_string_api! {
298298

299299
impl fmt::Display for GString {
300300
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301-
for ch in self.chars() {
302-
f.write_char(*ch)?;
303-
}
301+
pad_if_needed(f, |f| {
302+
for ch in self.chars() {
303+
f.write_char(*ch)?;
304+
}
304305

305-
Ok(())
306+
Ok(())
307+
})
306308
}
307309
}
308310

godot-core/src/builtin/string/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,48 @@ fn found_to_option(index: i64) -> Option<usize> {
155155
Some(index_usize)
156156
}
157157
}
158+
159+
// ----------------------------------------------------------------------------------------------------------------------------------------------
160+
// Padding, alignment and precision support
161+
162+
// Used by sub-modules of this module.
163+
use standard_fmt::pad_if_needed;
164+
165+
mod standard_fmt {
166+
use std::fmt;
167+
use std::fmt::Write;
168+
169+
pub fn pad_if_needed<F>(f: &mut fmt::Formatter<'_>, display_impl: F) -> fmt::Result
170+
where
171+
F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result,
172+
{
173+
let needs_format = f.width().is_some() || f.precision().is_some() || f.align().is_some();
174+
175+
// Early exit if no custom formatting is needed.
176+
if !needs_format {
177+
return display_impl(f);
178+
}
179+
180+
let ic = FmtInterceptor { display_impl };
181+
182+
let mut local_str = String::new();
183+
write!(&mut local_str, "{ic}")?;
184+
f.pad(&local_str)
185+
}
186+
187+
struct FmtInterceptor<F>
188+
where
189+
F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result,
190+
{
191+
display_impl: F,
192+
}
193+
194+
impl<F> fmt::Display for FmtInterceptor<F>
195+
where
196+
F: Fn(&mut fmt::Formatter<'_>) -> fmt::Result,
197+
{
198+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199+
(self.display_impl)(f)
200+
}
201+
}
202+
}

godot-core/src/global/print.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ macro_rules! inner_godot_msg {
5050

5151
/// Pushes a warning message to Godot's built-in debugger and to the OS terminal.
5252
///
53+
/// # See also
54+
/// [`godot_print!`](macro.godot_print.html) and [`godot_error!`](macro.godot_error.html).
55+
///
56+
/// Related to the utility function [`global::push_warning()`](crate::global::push_warning).
57+
///
5358
/// _Godot equivalent: [`@GlobalScope.push_warning()`](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-push-warning)_.
5459
#[macro_export]
5560
macro_rules! godot_warn {
@@ -60,6 +65,12 @@ macro_rules! godot_warn {
6065

6166
/// Pushes an error message to Godot's built-in debugger and to the OS terminal.
6267
///
68+
/// # See also
69+
/// [`godot_print!`](macro.godot_print.html) and [`godot_warn!`](macro.godot_warn.html).
70+
/// For script errors (less relevant in Rust), use [`godot_script_error!`](macro.godot_script_error.html).
71+
///
72+
/// Related to the utility function [`global::push_error()`][crate::global::push_error].
73+
///
6374
/// _Godot equivalent: [`@GlobalScope.push_error()`](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-push-error)_.
6475
#[macro_export]
6576
macro_rules! godot_error {
@@ -69,6 +80,13 @@ macro_rules! godot_error {
6980
}
7081

7182
/// Logs a script error to Godot's built-in debugger and to the OS terminal.
83+
///
84+
/// This is rarely needed in Rust; script errors are typically emitted by the GDScript parser.
85+
///
86+
/// # See also
87+
/// [`godot_error!`](macro.godot_error.html) for a general error message.
88+
///
89+
///
7290
#[macro_export]
7391
macro_rules! godot_script_error {
7492
($fmt:literal $(, $args:expr)* $(,)?) => {
@@ -78,6 +96,22 @@ macro_rules! godot_script_error {
7896

7997
/// Prints to the Godot console.
8098
///
99+
/// Automatically appends a newline character at the end of the message.
100+
///
101+
/// Used exactly like standard [`println!`]:
102+
/// ```no_run
103+
/// use godot::global::godot_print;
104+
///
105+
/// let version = 4;
106+
/// godot_print!("Hello, Godot {version}!");
107+
/// ```
108+
///
109+
/// # See also
110+
/// [`godot_print_rich!`](macro.godot_print_rich.html) for a slower alternative that supports BBCode, color and URL tags.
111+
/// To print Godot errors and warnings, use [`godot_error!`](macro.godot_error.html) and [`godot_warn!`](macro.godot_warn.html), respectively.
112+
///
113+
/// This uses the underlying [`global::print()`][crate::global::print] function, which takes a variable-length slice of variants.
114+
///
81115
/// _Godot equivalent: [`@GlobalScope.print()`](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-print)_.
82116
#[macro_export]
83117
macro_rules! godot_print {
@@ -92,7 +126,7 @@ macro_rules! godot_print {
92126

93127
/// Prints to the Godot console. Supports BBCode, color and URL tags.
94128
///
95-
/// Slower than [`godot_print!`].
129+
/// Slower than [`godot_print!`](macro.godot_print_rich.html).
96130
///
97131
/// _Godot equivalent: [`@GlobalScope.print_rich()`](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-print-rich)_.
98132
#[macro_export]
@@ -106,7 +140,24 @@ macro_rules! godot_print_rich {
106140
};
107141
}
108142

109-
/// Concatenates format-style into a `GString`.
143+
/// Concatenates format-style arguments into a `GString`.
144+
///
145+
/// Works similar to Rust's standard [`format!`] macro but returns a Godot `GString`.
146+
///
147+
/// # Example
148+
/// ```no_run
149+
/// use godot::builtin::GString;
150+
/// use godot::global::godot_str;
151+
///
152+
/// let name = "Player";
153+
/// let score = 100;
154+
/// let message: GString = godot_str!("The {name} scored {score} points!");
155+
/// ```
156+
///
157+
/// # See also
158+
/// This macro uses the underlying [`global::str()`][crate::global::str] function, which takes a variable-length slice of variants.
159+
///
160+
/// _Godot equivalent: [`@GlobalScope.str()`](https://docs.godotengine.org/en/stable/classes/class_@globalscope.html#class-globalscope-method-str)_.
110161
#[macro_export]
111162
macro_rules! godot_str {
112163
($fmt:literal $(, $args:expr)* $(,)?) => {

itest/rust/src/builtin_tests/string/gstring_test.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,14 @@ crate::generate_string_bytes_and_cstr_tests!(
279279
]
280280
);
281281

282+
crate::generate_string_standard_fmt_tests!(
283+
builtin: GString,
284+
tests: [
285+
gstring_display,
286+
gstring_standard_pad,
287+
]
288+
);
289+
282290
// ----------------------------------------------------------------------------------------------------------------------------------------------
283291
// Helpers
284292

itest/rust/src/builtin_tests/string/node_path_test.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn node_path_conversion() {
3131

3232
assert_eq!(string, back);
3333
}
34+
3435
#[itest]
3536
fn node_path_equality() {
3637
let string = NodePath::from("some string");
@@ -126,3 +127,11 @@ fn node_path_get_subname() {
126127
assert_eq!(path.get_subname(2), "".into());
127128
})
128129
}
130+
131+
crate::generate_string_standard_fmt_tests!(
132+
builtin: NodePath,
133+
tests: [
134+
node_path_display,
135+
node_path_standard_pad,
136+
]
137+
);

itest/rust/src/builtin_tests/string/string_name_test.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,11 @@ crate::generate_string_bytes_and_cstr_tests!(
175175
string_name_from_cstr_utf8,
176176
]
177177
);
178+
179+
crate::generate_string_standard_fmt_tests!(
180+
builtin: StringName,
181+
tests: [
182+
string_name_display,
183+
string_name_standard_pad,
184+
]
185+
);

itest/rust/src/builtin_tests/string/string_test_macros.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,35 @@ macro_rules! generate_string_bytes_and_cstr_tests {
162162
}
163163
};
164164
}
165+
166+
// Tests padding with the standard formatter.
167+
#[macro_export]
168+
macro_rules! generate_string_standard_fmt_tests {
169+
(
170+
builtin: $T:ty,
171+
tests: [
172+
$display:ident,
173+
$standard_pad:ident,
174+
]
175+
) => {
176+
#[itest]
177+
fn $display() {
178+
let s = <$T>::from("abcd");
179+
180+
assert_eq!(format!("{s}"), "abcd");
181+
}
182+
183+
#[itest]
184+
fn $standard_pad() {
185+
let s = <$T>::from("abcd");
186+
187+
// Padding with spaces + alignment.
188+
assert_eq!(format!("{s:<6}"), "abcd ");
189+
assert_eq!(format!("{s:>6}"), " abcd");
190+
191+
// Precision.
192+
assert_eq!(format!("{s:.2}"), "ab");
193+
assert_eq!(format!("{s:.3}"), "abc");
194+
}
195+
};
196+
}

0 commit comments

Comments
 (0)