diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm
index 8219b07300..4211bb8add 100644
--- a/code/modules/client/preference_setup/general/01_basic.dm
+++ b/code/modules/client/preference_setup/general/01_basic.dm
@@ -100,7 +100,7 @@
return TOPIC_REFRESH
else if(href_list["nickname"])
- var/raw_nickname = input(user, "Choose your character's nickname:", "Character Nickname") as text|null
+ var/raw_nickname = tgui_input_text(user, "Choose your character's nickname:", "Character Nickname", pref.nickname)
if (!isnull(raw_nickname) && CanUseTopic(user))
var/new_nickname = sanitize_name(raw_nickname, pref.species, is_FBP())
if(new_nickname)
@@ -140,7 +140,7 @@
return TOPIC_REFRESH
else if(href_list["metadata"])
- var/new_metadata = sanitize(input(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata)) as message|null, extra = 0) //VOREStation Edit
+ var/new_metadata = sanitize(tgui_input_message(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata)), extra = 0) //VOREStation Edit
if(new_metadata && CanUseTopic(user))
pref.metadata = new_metadata
return TOPIC_REFRESH
diff --git a/code/modules/client/preference_setup/vore/02_size.dm b/code/modules/client/preference_setup/vore/02_size.dm
index 19df50973f..95a944e931 100644
--- a/code/modules/client/preference_setup/vore/02_size.dm
+++ b/code/modules/client/preference_setup/vore/02_size.dm
@@ -87,19 +87,19 @@
return TOPIC_REFRESH
else if(href_list["weight_gain"])
- var/weight_gain_rate = input(user, "Choose your character's rate of weight gain between 100% \
+ var/weight_gain_rate = tgui_input_num(user, "Choose your character's rate of weight gain between 100% \
(full realism body fat gain) and 0% (no body fat gain).\n\
(If you want to disable weight gain, set this to 0.01 to round it to 0%.)\
- ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference") as num|null
+ ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_gain)
if(weight_gain_rate)
pref.weight_gain = round(text2num(weight_gain_rate),1)
return TOPIC_REFRESH
else if(href_list["weight_loss"])
- var/weight_loss_rate = input(user, "Choose your character's rate of weight loss between 100% \
+ var/weight_loss_rate = tgui_input_num(user, "Choose your character's rate of weight loss between 100% \
(full realism body fat loss) and 0% (no body fat loss).\n\
(If you want to disable weight loss, set this to 0.01 round it to 0%.)\
- ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference") as num|null
+ ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_loss)
if(weight_loss_rate)
pref.weight_loss = round(text2num(weight_loss_rate),1)
return TOPIC_REFRESH
diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm
new file mode 100644
index 0000000000..e2247a32ce
--- /dev/null
+++ b/code/modules/tgui/tgui_input_text.dm
@@ -0,0 +1,299 @@
+/**
+ * Creates a TGUI input text window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * default - The default value pre-populated in the input box.
+ * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_text(mob/user, message, title, default, timeout = 0)
+ if (istext(user))
+ stack_trace("tgui_input_text() received text for user instead of mob")
+ return
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout)
+ input.input_type = "text"
+ input.tgui_interact(user)
+ input.wait()
+ if (input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * Creates a TGUI input message window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * default - The default value pre-populated in the input box.
+ * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_message(mob/user, message, title, default, timeout = 0)
+ if (istext(user))
+ stack_trace("tgui_input_message() received text for user instead of mob")
+ return
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout)
+ input.input_type = "message"
+ input.tgui_interact(user)
+ input.wait()
+ if (input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * Creates a TGUI input num window and returns the user's response.
+ *
+ * This proc should be used to create alerts that the caller will wait for a response from.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * default - The default value pre-populated in the input box.
+ * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_num(mob/user, message, title, default, timeout = 0)
+ if (istext(user))
+ stack_trace("tgui_input_num() received text for user instead of mob")
+ return
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout)
+ input.input_type = "num"
+ input.tgui_interact(user)
+ input.wait()
+ if (input)
+ . = input.choice
+ qdel(input)
+
+/**
+ * Creates an asynchronous TGUI input text window with an associated callback.
+ *
+ * This proc should be used to create inputs that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * default - The default value pre-populated in the input box.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
+ if (istext(user))
+ stack_trace("tgui_input_text_async() received text for user instead of mob")
+ return
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout)
+ input.input_type = "text"
+ input.tgui_interact(user)
+
+/**
+ * Creates an asynchronous TGUI input message window with an associated callback.
+ *
+ * This proc should be used to create inputs that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * default - The default value pre-populated in the input box.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_message_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
+ if (istext(user))
+ stack_trace("tgui_input_message_async() received text for user instead of mob")
+ return
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout)
+ input.input_type = "message"
+ input.tgui_interact(user)
+
+/**
+ * Creates an asynchronous TGUI input num window with an associated callback.
+ *
+ * This proc should be used to create inputs that invoke a callback with the user's chosen option.
+ * Arguments:
+ * * user - The user to show the input box to.
+ * * message - The content of the input box, shown in the body of the TGUI window.
+ * * title - The title of the input box, shown on the top of the TGUI window.
+ * * default - The default value pre-populated in the input box.
+ * * callback - The callback to be invoked when a choice is made.
+ * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
+ */
+/proc/tgui_input_num_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
+ if (istext(user))
+ stack_trace("tgui_input_num_async() received text for user instead of mob")
+ return
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout)
+ input.input_type = "num"
+ input.tgui_interact(user)
+
+/**
+ * # tgui_input_dialog
+ *
+ * Datum used for instantiating and using a TGUI-controlled input that prompts the user with
+ * a message and a box for accepting text/message/num input.
+ */
+/datum/tgui_input_dialog
+ /// The title of the TGUI window
+ var/title
+ /// The textual body of the TGUI window
+ var/message
+ /// The default value to initially populate the input box.
+ var/initial
+ /// The value that the user input into the input box, null if cancelled.
+ var/choice
+ /// The time at which the tgui_text_input was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the tgui_text_input, after which the window will close and delete itself.
+ var/timeout
+ /// Boolean field describing if the tgui_text_input was closed by the user.
+ var/closed
+ /// Indicates the data type we want to collect ("text", "message", "num")
+ var/input_type = "text"
+
+/datum/tgui_input_dialog/New(mob/user, message, title, default, timeout)
+ src.title = title
+ src.message = message
+ // TODO - Do we need to sanitize the initial value for illegal characters?
+ src.initial = default
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_input_dialog/Destroy(force, ...)
+ SStgui.close_uis(src)
+ . = ..()
+
+/**
+ * Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_input_dialog/proc/wait()
+ while (!choice && !closed)
+ stoplag(1)
+
+/datum/tgui_input_dialog/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "InputModal")
+ ui.open()
+
+/datum/tgui_input_dialog/tgui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_input_dialog/tgui_state(mob/user)
+ return GLOB.tgui_always_state
+
+/datum/tgui_input_dialog/tgui_static_data(mob/user)
+ . = list(
+ "title" = title,
+ "message" = message,
+ "initial" = initial,
+ "input_type" = input_type
+ )
+
+/datum/tgui_input_dialog/tgui_data(mob/user)
+ . = list()
+ if(timeout)
+ .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
+
+/datum/tgui_input_dialog/tgui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("choose")
+ set_choice(params["choice"])
+ if(isnull(src.choice))
+ return
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ SStgui.close_uis(src)
+ closed = TRUE
+ return TRUE
+
+/datum/tgui_input_dialog/proc/set_choice(choice)
+ if(input_type == "num")
+ src.choice = text2num(choice)
+ return
+ src.choice = choice
+
+/**
+ * # async tgui_text_input
+ *
+ * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses.
+ */
+/datum/tgui_input_dialog/async
+ /// The callback to be invoked by the tgui_text_input upon having a choice made.
+ var/datum/callback/callback
+
+/datum/tgui_input_dialog/async/New(mob/user, message, title, default, callback, timeout)
+ ..(user, title, message, default, timeout)
+ src.callback = callback
+
+/datum/tgui_input_dialog/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_input_dialog/async/tgui_close(mob/user)
+ . = ..()
+ qdel(src)
+
+/datum/tgui_input_dialog/async/set_choice(choice)
+ . = ..()
+ if(!isnull(src.choice))
+ callback?.InvokeAsync(src.choice)
+
+/datum/tgui_input_dialog/async/wait()
+ return
diff --git a/tgui/packages/tgui/components/TextArea.js b/tgui/packages/tgui/components/TextArea.js
index 00e1605a3b..fb6ee17b12 100644
--- a/tgui/packages/tgui/components/TextArea.js
+++ b/tgui/packages/tgui/components/TextArea.js
@@ -104,6 +104,9 @@ export class TextArea extends Component {
if (input) {
input.value = toInputValue(nextValue);
}
+ if (this.props.autoFocus) {
+ setTimeout(() => input.focus(), 1);
+ }
}
componentDidUpdate(prevProps, prevState) {
diff --git a/tgui/packages/tgui/interfaces/InputModal.js b/tgui/packages/tgui/interfaces/InputModal.js
new file mode 100644
index 0000000000..62a5571085
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/InputModal.js
@@ -0,0 +1,116 @@
+/**
+ * @file
+ * @copyright 2021 Leshana
+ * @license MIT
+ */
+
+import { clamp01 } from 'common/math';
+import { useBackend, useLocalState } from '../backend';
+import { Box, Button, Section, Input, Stack, TextArea } from '../components';
+import { KEY_ESCAPE } from 'common/keycodes';
+import { Window } from '../layouts';
+import { createLogger } from '../logging';
+
+const logger = createLogger('inputmodal');
+
+export const InputModal = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { title, message, initial, input_type, timeout } = data;
+
+ // Current Input Value
+ const [curValue, setCurValue] = useLocalState(context, 'curValue', initial);
+
+ const handleKeyDown = e => {
+ if (e.keyCode === KEY_ESCAPE) {
+ e.preventDefault();
+ act("cancel");
+ return;
+ }
+ };
+
+ let initialHeight, initialWidth;
+ let modalBody;
+ switch (input_type) {
+ case 'text':
+ case 'num':
+ initialWidth = 325;
+ initialHeight = Math.max(150, message.length);
+ modalBody = (
+ {
+ setCurValue(val);
+ }}
+ onEnter={(_e, val) => {
+ act('choose', { choice: val });
+ }}
+ />
+ );
+ break;
+ case 'message':
+ initialWidth = 450;
+ initialHeight = 350;
+ modalBody = (
+