Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utils.showFloatingGamepadTextInput() #113

Merged
merged 3 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions client.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ export namespace utils {
export function getAppId(): number
export function getServerRealTime(): number
export function isSteamRunningOnSteamDeck(): boolean
export const enum GamepadTextInputMode {
Normal = 0,
Password = 1
}
export const enum GamepadTextInputLineMode {
SingleLine = 0,
MultipleLines = 1
}
/** @returns the entered text, or null if cancelled or could not show the input */
export function showGamepadTextInput(inputMode: GamepadTextInputMode, inputLineMode: GamepadTextInputLineMode, description: string, maxCharacters: number, existingText?: string | undefined | null): Promise<string | null>
export const enum FloatingGamepadTextInputMode {
SingleLine = 0,
MultipleLines = 1,
Email = 2,
Numeric = 3
}
/** @returns true if the floating keyboard was shown, otherwise, false */
export function showFloatingGamepadTextInput(keyboardMode: FloatingGamepadTextInputMode, x: number, y: number, width: number, height: number): Promise<boolean>
}
export namespace workshop {
export interface UgcResult {
Expand Down
112 changes: 112 additions & 0 deletions src/api/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ use napi_derive::napi;

#[napi]
pub mod utils {
use napi::bindgen_prelude::{FromNapiValue, ToNapiValue};
use steamworks::FloatingGamepadTextInputMode as kFloatingGamepadTextInputMode;
use steamworks::GamepadTextInputLineMode as kGamepadTextInputLineMode;
use steamworks::GamepadTextInputMode as kGamepadTextInputMode;
use tokio::sync::oneshot;

#[napi]
pub fn get_app_id() -> u32 {
let client = crate::client::get_client();
Expand All @@ -19,4 +25,110 @@ pub mod utils {
let client = crate::client::get_client();
client.utils().is_steam_running_on_steam_deck()
}

#[napi]
pub enum GamepadTextInputMode {
Normal,
Password,
}

#[napi]
pub enum GamepadTextInputLineMode {
SingleLine,
MultipleLines,
}

/// @returns the entered text, or null if cancelled or could not show the input
#[napi]
pub async fn show_gamepad_text_input(
input_mode: GamepadTextInputMode,
input_line_mode: GamepadTextInputLineMode,
description: String,
max_characters: u32,
existing_text: Option<String>,
) -> Option<String> {
let client = crate::client::get_client();

let (tx, rx) = oneshot::channel();
let mut tx = Some(tx);

let opened = client.utils().show_gamepad_text_input(
match input_mode {
GamepadTextInputMode::Normal => kGamepadTextInputMode::Normal,
GamepadTextInputMode::Password => kGamepadTextInputMode::Password,
},
match input_line_mode {
GamepadTextInputLineMode::SingleLine => kGamepadTextInputLineMode::SingleLine,
GamepadTextInputLineMode::MultipleLines => kGamepadTextInputLineMode::MultipleLines,
},
&description,
max_characters,
existing_text.as_deref(),
move |dismissed_data| {
if let Some(tx) = tx.take() {
let text = client
.utils()
.get_entered_gamepad_text_input(&dismissed_data);
tx.send(text).unwrap();
}
Comment on lines +68 to +73
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is no result here? The Promise will never resolve, right?

Looking further at the docs, this function returns true if "big picture overlay is running" but I'm wondering if that's a docs mistake and it returns true/false if the keyboard was opened or not similar to the floating text input function (or if it kinda means the same in this case, anyway).

Since the two new functions can be called without showing the gamepad, there is one more wrinkle in the Promise based API: Technically, promises are expected to be resolved at some point in the future, but if the gamepad doesn't open or doesn't exist, the Promise will never be handled. This is kinda ok-ish when using the Promise API, but it's very confusing when using async/await since the code block will just get stuck:

try {
  const result = await showGamepadTextInput();
  doSomething(result); // This will never be called.
} catch (error) {
  // This also never happens.
}

Most likely this is fine if it's documented. One thing to keep in mind is that this behavior is likely attached to input elements and is called repeatedly. Without knowing whether the gamepad can/is ever shown on the platform that it is being called on it will create a lot of dangling promises in memory that will stay unresolved for the duration of the app runtime.

Now, this might be an acceptable trade-off for you, but I wanted to bring it up anyway for consideration.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is no result here? The Promise will never resolve, right? (and the questions below that)

I don't think so, the memory management will drop the promise and unsign the callback as soon as the function returns something.

The idea is to always resolve the promise, either with the text input or with an undefined state, meaning the text input could not be shown or was cancelled. Example impl:

const input = await showGamepadTextInput();
if (input) {
    doSomething(input);
} else {
    // do nothing, as use cancelled the input request
}

Looking further at the docs, this function returns true if "big picture overlay is running" but I'm wondering if that's a docs mistake and it returns true/false if the keyboard was opened or not similar to the floating text input function (or if it kinda means the same in this case, anyway).

I think it means the same thing 🤔

},
);

if opened {
rx.await.unwrap()
} else {
None
}
}

#[napi]
pub enum FloatingGamepadTextInputMode {
SingleLine,
MultipleLines,
Email,
Numeric,
}

/// @returns true if the floating keyboard was shown, otherwise, false
#[napi]
pub async fn show_floating_gamepad_text_input(
keyboard_mode: FloatingGamepadTextInputMode,
x: i32,
y: i32,
width: i32,
height: i32,
) -> bool {
let client = crate::client::get_client();

let (tx, rx) = oneshot::channel();
let mut tx = Some(tx);

let opened = client.utils().show_floating_gamepad_text_input(
match keyboard_mode {
FloatingGamepadTextInputMode::SingleLine => {
kFloatingGamepadTextInputMode::SingleLine
}
FloatingGamepadTextInputMode::MultipleLines => {
kFloatingGamepadTextInputMode::MultipleLines
}
FloatingGamepadTextInputMode::Email => kFloatingGamepadTextInputMode::Email,
FloatingGamepadTextInputMode::Numeric => kFloatingGamepadTextInputMode::Numeric,
},
x,
y,
width,
height,
move || {
if let Some(tx) = tx.take() {
tx.send(true).unwrap();
}
},
);

if opened {
rx.await.unwrap()
} else {
false
}
Comment on lines +121 to +132
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding the docs correctly, this function will immediately return true/false if the keyboard was shown or not, and invoke the dismissed callback once the keyboard is dismissed by user. The API might need some adjustments in that case as this promise based API doesn't behave the same way. An alternative solution would be to return false | Promise<void> to denote that either it wasn't opened (and there is no need to listen for the dismissed event), or a tuple/object like [opened: boolean, onDismissed: Promise<void>] for a cleaner state separation.

Let me know if I'm missing something.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see a real use case where you will need both, I think the promise way maes more sense on JS and can benefit the consumer for a more fluid code flow. What do you think?

}
}
Loading