Mostly finished VN textbox

This commit is contained in:
2025-09-16 23:27:02 -05:00
parent 96b5bae9d0
commit 1e83200bba
7 changed files with 132 additions and 126 deletions

View File

@@ -1,51 +1,49 @@
class_name VNTextbox extends PanelContainer
const VN_REVEAL_TIME = 0.01
const VISIBLE_LINES:int = 4
var label:RichTextLabel;
var text:String = ""
var label:AdvancedRichText;
var parsedOutText = ""
var visibleCharacters:int = 0;
var revealTimer:float = 0;
var lineStarts:Array[int] = [];
var newlineIndexes:Array[int] = [];
var wrappedText:String = "";
var currentLine = 0;
var currentViewScrolled = true;
var isSpeedupDown = false;
var isMoreViews = false;
var isClosed = true;
var isClosed:bool = false:
get():
return !self.visible;
set(value):
self.visible = !value;
signal textboxClosing
func _ready() -> void:
label = $MarginContainer/Label
self.visible = false;
isClosed = true
func _process(delta: float) -> void:
if text == "":
if label.getFinalText() == "":
isClosed = true;
return;
if currentViewScrolled:
if Input.is_action_just_pressed("interact"):
if isMoreViews:
visibleCharacters = 0;
currentLine += VISIBLE_LINES;
if label.visible_characters >= label.getCharactersDisplayedCount():
if (label.maxLines + label.startLine) < label.getTotalLineCount():
# Not on last page.
if Input.is_action_just_pressed("interact"):
label.startLine += label.maxLines;
label.visible_characters = 0;
currentViewScrolled = false;
recalculateWrapping();
return
setText("");
textboxClosing.emit();
return;
if visibleCharacters >= getCountOfCharactersToScrollInView():
currentViewScrolled = true;
#print("Scrolled view");
#if isMoreViews:
#print("More views");
currentViewScrolled = true;
else:
# On last page
if Input.is_action_just_released("interact"):
textboxClosing.emit();
isClosed = true;
currentViewScrolled = true
return;
if Input.is_action_just_pressed("interact"):
@@ -61,63 +59,15 @@ func _process(delta: float) -> void:
if revealTimer > VN_REVEAL_TIME:
revealTimer = 0;
visibleCharacters += 1;
label.visible_characters = visibleCharacters;
func getCountOfCharactersToScrollInView() -> int:
if lineStarts.size() <= VISIBLE_LINES:
return wrappedText.length();
if currentLine + VISIBLE_LINES >= lineStarts.size():
return wrappedText.length() - lineStarts[currentLine];
return lineStarts[min(lineStarts.size(), currentLine + VISIBLE_LINES)] - lineStarts[currentLine];
func recalculateWrapping():
# Reset label to default.
label.advancedText = text;
label.visible_characters = -1;
label.fit_content = true;
isMoreViews = false;
# Determine where the wrapped newlines are
lineStarts = [ 0 ];
var line = 0;
var wasNewLine = false;
for i in range(0, text.length()):
var tLine = label.get_character_line(i);
if tLine == line:
wasNewLine = false
if text[i] == "\n":
wasNewLine = true
continue;
if !wasNewLine:
newlineIndexes.append(i);
lineStarts.append(i);
line = tLine;
# Create fake pre-wrapped text.
wrappedText = "";
for i in range(lineStarts[currentLine], text.length()):
if newlineIndexes.find(i) != -1 and i != lineStarts[currentLine]:
wrappedText += "\n";
wrappedText += text[i];
label.advancedText = wrappedText;
label.fit_content = false;
label.visible_characters = 0;
if lineStarts.size() > currentLine + VISIBLE_LINES:
isMoreViews = true;
label.visible_characters += 1;
func setText(text:String) -> void:
self.text = text;
if text == "":
self.visible = false;
return;
isClosed = false;
revealTimer = 0;
visibleCharacters = 0;
currentLine = 0;
currentViewScrolled = false;
recalculateWrapping();
self.visible = true;
label.visible_characters = 0;
label.startLine = 0;
label.text = ""
await get_tree().process_frame# Absolutely needed to make the text wrap
label.text = text;
label.visible_characters = 0;

View File

@@ -1,11 +1,11 @@
[gd_scene load_steps=5 format=3 uid="uid://bkx3l0kckf4a8"]
[gd_scene load_steps=4 format=3 uid="uid://bkx3l0kckf4a8"]
[ext_resource type="Theme" uid="uid://dm7ee4aqjr2dl" path="res://ui/UI Theme.tres" id="1_wx4lp"]
[ext_resource type="Script" uid="uid://h8lw23ypcfty" path="res://ui/component/VNTextbox.gd" id="2_uo1gm"]
[ext_resource type="Script" uid="uid://c6av3xe4m2ujl" path="res://addons/madtalk/runtime/madtalk_runtime.gd" id="3_2x6s1"]
[ext_resource type="Script" uid="uid://bjj6upgk1uvxd" path="res://ui/component/advancedrichtext/AdvancedRichText.gd" id="3_m60k3"]
[node name="VNTextbox" type="PanelContainer"]
clip_contents = true
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
@@ -16,13 +16,8 @@ grow_vertical = 0
theme = ExtResource("1_wx4lp")
script = ExtResource("2_uo1gm")
[node name="MadTalk" type="Node" parent="."]
script = ExtResource("3_2x6s1")
DialogMainControl = NodePath("..")
DialogMessageBox = NodePath("../MarginContainer")
DialogMessageLabel = NodePath("../MarginContainer/Label")
[node name="MarginContainer" type="MarginContainer" parent="."]
clip_contents = true
layout_mode = 2
theme = ExtResource("1_wx4lp")
theme_override_constants/margin_left = 4
@@ -34,8 +29,43 @@ theme_override_constants/margin_bottom = 4
layout_mode = 2
theme = ExtResource("1_wx4lp")
bbcode_enabled = true
text = "To get through tight spaces, press[input action=down][/input]"
fit_content = true
text = "Hello, I'm an NPC!
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10"
script = ExtResource("3_m60k3")
userText = "To get through tight spaces, press[input action=down][/input]"
finalText = "To get through tight spaces, press[img height=12 valign=center,center]res://ui/input/down.tres[/img]"
userText = "Hello, I'm an NPC!
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10"
_finalText = "Hello, I'm an NPC!
This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot's built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Line 7
Line 8
Line 9
Line 10"
_newLineIndexes = Array[int]([0])
_lines = PackedStringArray("Hello, I\'m an NPC!", "This is the second line here, I am purposefully adding a tonne of words so that it is forced to go across multiple lines and you can see how the word wrapping works, not only using Godot\'s built in word wrapping but with my advanced visibile characters smart wrapping. Now I am doing a multiline thing", "Line 1", "Line 2", "Line 3", "Line 4", "Line 5", "Line 6", "Line 7", "Line 8", "Line 9", "Line 10")
maxLines = 4

View File

@@ -2,7 +2,10 @@
class_name AdvancedRichText extends RichTextLabel
@export_multiline var userText:String = "" # The text the user is asking for
@export_multiline var finalText:String = "" # The final text after processing (translation, wrapping, etc.)
@export_multiline var _finalText:String = "" # The final text after processing (translation, wrapping, etc.)
@export var _newLineIndexes:Array[int] = [] # The indexes of where each line starts in finalText
@export var _lines:PackedStringArray = [];
# Hides the original RichTextLabel text property
func _set(property: StringName, value) -> bool:
@@ -36,17 +39,43 @@ func _get(property: StringName):
get():
return smartWrap
func _init() -> void:
_recalcText()
@export var maxLines:int = -1:
set(value):
maxLines = value
_recalcText()
get():
return maxLines
@export var startLine:int = 0:
set(value):
startLine = value
_recalcText()
get():
return startLine
# Returns count of characters that can be displayed, assuming visible_chars = -1
func getCharactersDisplayedCount() -> int:
# Count characters
var count = 0
var lineCount = min(startLine + maxLines, _lines.size()) - startLine
for i in range(startLine, startLine + lineCount):
count += _lines[i].length()
if lineCount > 1:
count += lineCount - 1 # Add newlines
return count
func getFinalText() -> String:
return _finalText
func getTotalLineCount() -> int:
return _lines.size()
func _enter_tree() -> void:
_recalcText()
self.resized.connect(_recalcText)
func _exit_tree() -> void:
self.resized.disconnect(_recalcText)
self.threaded = false;
func _recalcText() -> void:
_lines.clear()
if userText.is_empty():
self.richtextlabel_text = ""
return
@@ -63,8 +92,6 @@ func _recalcText() -> void:
for match in regex.search_all(textTranslated):
var action = match.get_string(1).to_lower()
var height:int = get_theme_font_size("normal_font_size")
# var device = get_current_device_type()
# var icon_path = get_icon_for_action(action, device)
var img_tag = "[img height=%d valign=center,center]res://ui/input/%s.tres[/img]" % [ height, action ]
inputIconText = inputIconText.replace(match.get_string(0), img_tag)
@@ -76,29 +103,39 @@ func _recalcText() -> void:
self.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART;
self.richtextlabel_text = unwrappedText
self.visible_characters = -1;
self.fit_content = true;
var newlineIndexes = [];
self.fit_content = false;
_newLineIndexes = [];
# Determine where the wrapped newlines are
var line = 0;
var wasNewLine = false;
for i in range(0, unwrappedText.length()):
for i in range(0, self.richtextlabel_text.length()):
var tLine = self.get_character_line(i);
if tLine == line || tLine == -1:
if tLine == line:
wasNewLine = false
if unwrappedText[i] == "\n":
if self.richtextlabel_text[i] == "\n":
wasNewLine = true
continue;
if !wasNewLine:
newlineIndexes.append(i);
_newLineIndexes.append(i);
line = tLine;
# Create fake pre-wrapped text.
wrappedText = "";
for i in range(0, unwrappedText.length()):
if newlineIndexes.find(i) != -1 and i != 0:
for i in range(0, self.richtextlabel_text.length()):
if _newLineIndexes.find(i) != -1 and i != 0:
wrappedText += "\n";
wrappedText += unwrappedText[i];
wrappedText += self.richtextlabel_text[i];
finalText = wrappedText
self.richtextlabel_text = wrappedText
# Handle max and start line(s)
var maxText = wrappedText
if maxLines > 0:
_lines = maxText.split("\n", true);
var selectedLines = [];
for i in range(startLine, min(startLine + maxLines, _lines.size())):
selectedLines.append(_lines[i]);
maxText = "\n".join(selectedLines);
_finalText = maxText
self.richtextlabel_text = maxText
print("Updated text")