Skip to content

Implement From<char> for GString#1554

Open
QuinnPainter wants to merge 1 commit into
godot-rust:masterfrom
QuinnPainter:gstring_from_char
Open

Implement From<char> for GString#1554
QuinnPainter wants to merge 1 commit into
godot-rust:masterfrom
QuinnPainter:gstring_from_char

Conversation

@QuinnPainter
Copy link
Copy Markdown

Little convenience function I've needed a few times.

@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Apr 12, 2026

Thanks for your contribution.

Could you please elaborate where/how you've needed this?

@Bromeon Bromeon added quality-of-life No new functionality, but improves ergonomics/internals c: core Core components labels Apr 12, 2026
@QuinnPainter
Copy link
Copy Markdown
Author

Recently I was working on an on-screen keyboard, and it has some places where single chars need to be used with Godot functions. For example, pressing a key inserts a char using LineEdit::insert_text_at_caret.

Probably not something that would be used a ton, but it seems like a reasonable / intuitive conversion to be included. std::string also implements From<char>.

@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Apr 12, 2026

For example, pressing a key inserts a char using LineEdit::insert_text_at_caret.

Can you provide some example code? I'm asking because there's (AFAIK) no API in Godot that returns Rust's char type.

@QuinnPainter
Copy link
Copy Markdown
Author

QuinnPainter commented Apr 12, 2026

Edit bromeon: rs codetags

No, I'm not using chars returned from any Godot API.

I have a Rust array of chars representing a keyboard layout. At startup, that array is used to instantiate a grid of KeyboardKey objects:

#[derive(GodotClass)]
#[class(base=Button, no_init)]
pub struct OnScreenKeyboardKey {
    ty: KeyType,
    kb: Gd<OnScreenKeyboard>,
    base: Base<Button>,
}

where KeyType includes which char it corresponds to:

enum KeyType {
    Char(char, char), // lowercase, uppercase
    Done,
    Backspace,
    MoveLeft,
    MoveRight,
    Shift,
    Caps,
}

Then, when a key is pressed, it inserts that stored char into the LineEdit.

        match key {
            KeyType::Char(lower, upper) => {
                let out_char = if uppercase_state { upper } else { lower };
                self.shift_engaged = false;

                self.text_preview
                    .insert_text_at_caret(&GString::from([out_char].as_slice()));
            }
            ....
        }

Here's the full (unfinished) code

Details
use godot::{
    classes::{Button, GridContainer, IButton, IPanelContainer, LineEdit, PanelContainer, base_button::ActionMode},
    prelude::*,
};

#[derive(GodotClass)]
#[class(base=PanelContainer, init)]
pub struct OnScreenKeyboard {
    shift_engaged: bool,
    caps_engaged: bool,
    #[export]
    text_preview: OnEditor<Gd<LineEdit>>,
    key_buttons: Vec<Gd<OnScreenKeyboardKey>>,
    base: Base<PanelContainer>,
}

#[derive(Copy, Clone)]
enum KeyType {
    Char(char, char), // lowercase, uppercase
    Done,
    Backspace,
    MoveLeft,
    MoveRight,
    Shift,
    Caps,
}

#[godot_api]
impl IPanelContainer for OnScreenKeyboard {
    fn ready(&mut self) {
        let kb_layout = [
            KeyType::Char('1', '1'),
            KeyType::Char('2', '2'),
            KeyType::Char('3', '3'),
            KeyType::Char('4', '4'),
            KeyType::Char('5', '5'),
            KeyType::Char('6', '6'),
            KeyType::Char('7', '7'),
            KeyType::Char('8', '8'),
            KeyType::Char('9', '9'),
            KeyType::Char('0', '0'),
            KeyType::Char('q', 'Q'),
            KeyType::Char('w', 'W'),
            KeyType::Char('e', 'E'),
            KeyType::Char('r', 'R'),
            KeyType::Char('t', 'T'),
            KeyType::Char('y', 'Y'),
            KeyType::Char('u', 'U'),
            KeyType::Char('i', 'I'),
            KeyType::Char('o', 'O'),
            KeyType::Char('p', 'P'),
            KeyType::Char('a', 'A'),
            KeyType::Char('s', 'S'),
            KeyType::Char('d', 'D'),
            KeyType::Char('f', 'F'),
            KeyType::Char('g', 'G'),
            KeyType::Char('h', 'H'),
            KeyType::Char('j', 'J'),
            KeyType::Char('k', 'K'),
            KeyType::Char('l', 'L'),
            KeyType::Char('?', '?'),
            KeyType::Char('z', 'Z'),
            KeyType::Char('x', 'X'),
            KeyType::Char('c', 'C'),
            KeyType::Char('v', 'V'),
            KeyType::Char('b', 'B'),
            KeyType::Char('n', 'N'),
            KeyType::Char('m', 'M'),
            KeyType::Char('-', '-'),
            KeyType::Char('_', '_'),
            KeyType::Char('!', '!'),
            KeyType::MoveLeft,
            KeyType::MoveRight,
            KeyType::Shift,
            KeyType::Caps,
            KeyType::Char(' ', ' '),
            KeyType::Char('.', '.'),
            KeyType::Char(',', ','),
            KeyType::Char('#', '#'),
            KeyType::Backspace,
            KeyType::Done,
        ];

        let mut grid_container = self.base().get_node_as::<GridContainer>("VBoxContainer/GridContainer");
        self.key_buttons.reserve_exact(kb_layout.len());
        for key in kb_layout {
            let mut key_button = OnScreenKeyboardKey::new(key, self.to_gd());
            key_button.set_custom_minimum_size(Vector2::new(50.0, 50.0));
            key_button.set_action_mode(ActionMode::PRESS);
            grid_container.add_child(&key_button);
            self.key_buttons.push(key_button);
        }

        self.update_key_text();
    }
}

#[godot_api]
impl OnScreenKeyboard {
    fn using_uppercase(&self) -> bool {
        self.caps_engaged ^ self.shift_engaged
    }

    fn update_key_text(&mut self) {
        let uppercase = self.using_uppercase();
        for key_button in &mut self.key_buttons {
            key_button.bind_mut().update_text(uppercase);
        }
    }

    fn key_pressed(&mut self, key: KeyType) {
        let uppercase_state = self.using_uppercase();

        match key {
            KeyType::Char(lower, upper) => {
                let out_char = if uppercase_state { upper } else { lower };
                self.shift_engaged = false;

                self.text_preview
                    .insert_text_at_caret(&GString::from([out_char].as_slice()));
            }
            KeyType::Done => (),
            KeyType::Backspace => {
                let caret_pos = self.text_preview.get_caret_column();
                if caret_pos > 0 {
                    self.text_preview.delete_text(caret_pos - 1, caret_pos);
                }
            }
            KeyType::MoveLeft => {
                let caret_pos = self.text_preview.get_caret_column();
                self.text_preview.set_caret_column(caret_pos - 1);
            }
            KeyType::MoveRight => {
                let caret_pos = self.text_preview.get_caret_column();
                self.text_preview.set_caret_column(caret_pos + 1);
            }
            KeyType::Caps => self.caps_engaged ^= true,
            KeyType::Shift => self.shift_engaged ^= true,
        }

        if uppercase_state != self.using_uppercase() {
            self.update_key_text();
        }
    }
}

#[derive(GodotClass)]
#[class(base=Button, no_init)]
pub struct OnScreenKeyboardKey {
    ty: KeyType,
    kb: Gd<OnScreenKeyboard>,
    base: Base<Button>,
}

#[godot_api]
impl IButton for OnScreenKeyboardKey {
    fn ready(&mut self) {
        self.base().signals().pressed().connect_other(self, Self::pressed);
    }
}

impl OnScreenKeyboardKey {
    fn new(ty: KeyType, kb: Gd<OnScreenKeyboard>) -> Gd<Self> {
        Gd::from_init_fn(|base| Self { ty, base, kb })
    }

    fn pressed(&mut self) {
        // Needs to run deferred to avoid double-bind since it binds the keys
        let ty = self.ty;
        self.kb.bind_mut().run_deferred(move |s| s.key_pressed(ty));
    }

    fn update_text(&mut self, uppercase: bool) {
        let txt = match self.ty {
            KeyType::Char(lower, upper) => [if uppercase { upper } else { lower }].as_slice().into(),
            KeyType::Done => GString::from("Done"),
            KeyType::Backspace => GString::from("⌫"),
            KeyType::MoveLeft => GString::from("←"),
            KeyType::MoveRight => GString::from("→"),
            KeyType::Shift => GString::from("Shift"),
            KeyType::Caps => GString::from("Caps"),
        };
        self.base_mut().set_text(&txt);
    }
}

@GodotRust
Copy link
Copy Markdown

API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1554

@Yarwin
Copy link
Copy Markdown
Contributor

Yarwin commented Apr 13, 2026

If we decide to proceed with it, shouldn't we implement AsArg<GString> and AsArg<StringName> for char too?

impl_asarg_string!(GString);
impl_asarg_string!(StringName);
impl_asarg_string!(NodePath);

@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Apr 13, 2026

If we decide to proceed with it, shouldn't we implement AsArg<GString> and AsArg<StringName> for char too?

// now:
text_preview.insert_text_at_caret(&GString::from([out_char].as_slice()));

// with From:
text_preview.insert_text_at_caret(&GString::from(out_char));

// with AsArg:
text_preview.insert_text_at_caret(out_char);

It looks definitely nice.

But we also need to keep in mind that while convenient, implicit conversions (through AsArg) make things less explicit. For example, if someone has both char and String in scope, it's possible that they accidentally pass the wrong one to a Godot API, without errors. This problem exists with From to a lesser degree, too -- but at least the code shows that there is some conversion (since the impl Into isn't parameter-side).


One reason why I'm not 100% convinced by the change here is that I planned already a few times to add named conversion functions, most recently as a replacement for #1526. This could look like string_name.to_gstring(), and if we do it via extension trait, possibly even "str".to_gstring() and so on. Or in this case:

// with StringExt:
text_preview.insert_text_at_caret(out_char.to_gstring());

So if we one day move away from From -- in line with some existing API design -- the From<char> impl would introduce another breaking change 🤔

@Bromeon
Copy link
Copy Markdown
Member

Bromeon commented Apr 29, 2026

Sorry for the lack of feedback on this; our plan is to experiment with an extension trait for string conversions and see if this could reasonably covered by it, and then re-evaluate. We'll post here once there's an update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: core Core components quality-of-life No new functionality, but improves ergonomics/internals

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants