
Godot Ui Rich Text
- 232 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-ui-rich-text for development tasks
About
godot-ui-rich-text: A skill for development. This provides functionality for development workflows.
- godot-ui-rich-text
Godot Ui Rich Text by the numbers
- 232 all-time installs (skills.sh)
- +20 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,706 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-ui-rich-textAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 232 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-ui-rich-text for development tasks
Files
Rich Text & BBCode
BBCode tags, meta clickable links, and RichTextEffect shaders define formatted text systems.
rich_text_rainbow_effect.gd
Expert custom RichTextEffect that rotates colors over time.
rich_text_glitch_effect.gd
Professional horror-style glitch effects with spatial jitter and alpha flickering.
rich_text_typewriter_controller.gd
Dialogue manager that parses sequential event tags ([pause], [speed]) during animations.
rich_text_meta_dispatch.gd
Advanced handling for multi-prefix URLs in meta-clicks (items, quests, NPCs).
rich_text_image_scaler.gd
Utility to dynamically scale [img] tags to match runtime font sizes.
rich_text_hover_reactive.gd
Signals and logic for making text spans reactive to mouse hover (SFX/Cursors).
rich_text_bbcode_sanitizer.gd
Security utility to prevent BBCode injection in public chat interfaces.
rich_text_gradient_generator.gd
Generator for multi-stop linear gradients using granular character-level tagging.
rich_text_auto_scroller.gd
Smooth vertical auto-scrolling logic for credits, news feeds, and logs.
rich_text_syntax_highlighter.gd
Simple regex-based syntax highlighting pattern for code blocks in UI.
NEVER Do (Expert UI Rules)
Formatting & Rendering
- NEVER use complex BBCode in tight loops — Parsing a 10,000 character string with 500 tags every frame will tank performance. Cache your formatted strings.
- NEVER forget to register Custom Effects — Writing the script isn't enough. You MUST add the instance to
RichTextLabel.custom_effectslist via Inspector orinstall_effect(). - NEVER use absolute pixel sizes in [img] —
[img width=128]fails on higher resolutions. Userich_text_image_scaler.gdto sync with line height.
Click & Hover UX
- NEVER use [url] without visual feedback — If the text doesn't change color on hover or the cursor doesn't change, players won't know it's clickable. Use
rich_text_hover_reactive.gd. - NEVER hardcode layout logic into strings; strictly use BBCode Tables and Alignment Tags to ensure text structures remain flexible.
- NEVER animate text typewriter effects by modifying the
textorbbcodestring frame-by-frame; strictly use `visible_ratio` or `visible_characters` to avoid expensive parsing overhead and flickering. - NEVER use standard bitmap fonts for large titles or dynamic UI; strictly use MSDF (Multichannel Signed Distance Field) fonts to ensure perfectly crisp outlines and scaling at any resolution.
- NEVER perform heavy logic inside `meta_clicked` — This signal is on the Main Thread. Use it to emit a command and handle processing asynchronously if needed.
Dialogue & Narrative
- NEVER use `visible_ratio` for pausing typewriter —
visible_ratiois unreliable for per-character logic. Usevisible_charactersand explicit character indexing (rich_text_typewriter_controller.gd). - NEVER allow unfiltered user input in Chat Labels — A user could type
[img]huge_image_path[/img]or[color=transparent]to break your UI. ALWAYS userich_text_bbcode_sanitizer.gd.
---
$RichTextLabel.bbcode_enabled = true
$RichTextLabel.text = "[b]Bold[/b] and [i]italic[/i] text"Common Tags
[b]Bold[/b]
[i]Italic[/i]
[u]Underline[/u]
[color=red]Red text[/color]
[color=#00FF00]Green hex[/color]
[center]Centered[/center]
[img]res://icon.png[/img]
[url=data]Clickable link[/url]Handle Link Clicks
func _ready() -> void:
$RichTextLabel.meta_clicked.connect(_on_meta_clicked)
func _on_meta_clicked(meta: Variant) -> void:
print("Clicked: ", meta)Expert Text Patterns
1. Rich-Text-MSDF-Outline (SDF)
Enable crisp, high-resolution outlines and scaling by enabling MSDF on font resources and using theme overrides.
# msdf_styler.gd
func _ready():
# Crisp outlines regardless of screen scale
label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 4)2. Animated-Text-Reveal (Visible Ratio)
Efficient typewriter effect that preserves BBCode animations (like [wave] or [shake]) and avoids parsing overhead.
# dialogue_revealer.gd
func reveal(new_text: String):
label.text = new_text
label.visible_ratio = 0.0 # Set text but hide characters
var duration = label.get_total_character_count() / char_speed
# Animate ratio from 0 to 1
create_tween().tween_property(label, "visible_ratio", 1.0, duration)3. Custom-BBCode-Effect (RichTextEffect)
Define custom visual tags (like [relic]) by extending RichTextEffect for unique gameplay-themed text animations.
# relic_effect.gd
@tool
extends RichTextEffect
var bbcode = "relic"
func _process_custom_fx(char_fx: CharFXTransform):
# Retrieve param: [relic color=#ff00ff]
var color = char_fx.env.get("color", Color.GOLD)
# Apply sinusoidal floating
char_fx.offset.y += sin(char_fx.elapsed_time * 5.0) * 2.0
char_fx.color = color
return trueReference
Related
- Master Skill: godot-master
# skills/ui-rich-text/code/custom_bbcode_effect.gd
extends RichTextEffect
class_name RichTextRainbow
## UI RichText Expert Pattern
## Implements Custom RichTextEffect and Metadata Handling.
# 1. Custom BBCode Tags
# Tag usage: [rainbow freq=1.0 sat=0.8 val=0.8]Text[/rainbow]
var bbcode = "rainbow"
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
# Expert logic: Manipulate character properties over time.
var freq = char_fx.env.get("freq", 1.0)
var sat = char_fx.env.get("sat", 0.8)
var val = char_fx.env.get("val", 0.8)
var hue = fmod(char_fx.elapsed_time * freq + char_fx.range_index * 0.1, 1.0)
char_fx.color = Color.from_hsv(hue, sat, val)
return true
# 2. Programmatic Tag Injection (Keyword Highlighting)
func highlight_keywords(text: String, keywords: Array[String], color_hex: String) -> String:
# Professional protocol: Use regex to wrap keywords in BBCode tags.
var result = text
for word in keywords:
var regex = RegEx.new()
regex.compile("\\b" + word + "\\b")
result = regex.sub(result, "[color=#" + color_hex + "][b]" + word + "[/b][/color]", true)
return result
# 3. Meta-Intent Handling
func _on_meta_clicked(meta: Variant) -> void:
# Professional protocol: Handle interactive text links (e.g. Quest items).
if meta is String:
print("Player clicked on a RichText link: ", meta)
# Signal the GameController or DialogueSystem
# DialogueEventBus.text_link_activated.emit(meta)
## EXPERT NOTE:
## Use 'Animated Typewriter Effects': Combine 'visible_ratio' with
## custom 'char_fx' to create ghostly fades or jittery typewriter
## motion for horror or sci-fi dialogue.
## For 'ui-rich-text', implement a 'Dynamic Color Bus': Use BBCode
## colors that reference a global Theme variable via a lookup script
## to allow for "Night Mode" text color swaps.
## NEVER build complex UI layouts using only RichTextLabel;
## use it ONLY for body text and use Containers for buttons
## and iconography to ensure responsive layouts.
## Use 'bbcode_enabled = true' and 'install_effect()' to register
## your custom effects at runtime.
# skills/ui-rich-text/scripts/rich_text_animator.gd
extends RichTextLabel
## RichTextLabel Animator Expert Pattern
## Typewriter effect with custom BBCode event handling and pauses.
class_name RichTextAnimator
signal message_finished
signal character_displayed(char_index: int)
signal custom_tag_encountered(tag: String)
@export var speed_chars_per_sec := 50.0
@export var punctuation_pause := 0.4
var _target_visible_ratio := 0.0
var _current_text := ""
var _is_typing := false
func show_text(bbcode_text: String) -> void:
text = bbcode_text
visible_ratio = 0.0
_target_visible_ratio = 1.0
_is_typing = true
# Start tweening
var tween := create_tween()
var total_chars := get_total_character_count()
var duration := total_chars / speed_chars_per_sec
# Create a method tween to handle granular logic (pauses)
tween.tween_method(_update_visible_chars, 0.0, 1.0, duration)
tween.finished.connect(_on_finished)
func _update_visible_chars(ratio: float) -> void:
visible_ratio = ratio
var char_count = get_total_character_count()
var current_index = int(ratio * char_count)
character_displayed.emit(current_index)
# Handle pauses for punctuation (This is a simplified example)
# For robust pauses, you would pre-scan the text for custom tags like [pause=1.0]
func _on_finished() -> void:
_is_typing = false
visible_ratio = 1.0
message_finished.emit()
func install_custom_effect(effect: RichTextEffect) -> void:
if not custom_effects.has(effect):
custom_effects.append(effect)
## EXPERT USAGE:
## @onready var label = $RichTextAnimator
## label.show_text("Hello [wave]World[/wave]!")
## await label.message_finished
class_name RichTextAutoScroller
extends RichTextLabel
## Expert Vertical Auto-Scroll (Credits/Logs).
## Automatically advances the scroll bar smoothly.
@export var scroll_speed: float = 30.0 # Pixels per second
@export var pause_on_hover: bool = true
func _process(delta: float) -> void:
if pause_on_hover and get_global_rect().has_point(get_global_mouse_position()):
return
var v_scroll = get_v_scroll_bar()
v_scroll.value += scroll_speed * delta
if v_scroll.value >= v_scroll.max_value - v_scroll.page:
# Optionally loop or stop
pass
class_name RichTextBBCodeSanitizer
extends RefCounted
## Expert BBCode Sanitizer.
## Strips potentially malicious or layout-breaking tags from user input.
static func sanitize(input: String, allow_list: Array[String] = ["b", "i", "u", "color"]) -> String:
var result = input
var regex = RegEx.new()
# Match any tag starting with [
regex.compile("\\[/?([a-z0-9_]+)[^\\]]*\\]")
for m in regex.search_all(input):
var tag_name = m.get_string(1)
if not allow_list.has(tag_name):
result = result.replace(m.get_string(), "")
return result
@tool
class_name RichTextGlitchEffect
extends RichTextEffect
## Expert Glitch/Horror Text Effect.
## Syntax: [glitch_fx level=2.0]Scary Text[/glitch_fx]
var bbcode := "glitch_fx"
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
var intensity: float = char_fx.env.get("level", 2.0)
# High-frequency jitter
var rng := RandomNumberGenerator.new()
rng.seed = hash(char_fx.relative_index + int(char_fx.elapsed_time * 15.0))
char_fx.offset = Vector2(
rng.randf_range(-intensity, intensity),
rng.randf_range(-intensity, intensity)
)
# Random flickering
if rng.randf() > 0.8:
char_fx.color.a *= 0.3
return true
class_name RichTextGradientGenerator
extends RefCounted
## Expert Multi-Stop Gradient BBCode Generator.
## Wraps a string in granular [color] tags to create a smooth gradient.
static func generate(text: String, color_start: Color, color_end: Color) -> String:
var result := ""
var length := text.length()
for i in range(length):
var t := float(i) / float(length - 1) if length > 1 else 0.5
var col := color_start.lerp(color_end, t)
result += "[color=#%s]%s[/color]" % [col.to_html(false), text[i]]
return result
class_name RichTextHoverReactive
extends RichTextLabel
## Expert Mouse-Reactive Text Spans.
## Triggers sound and cursor changes when hovering over [url].
@export var hover_sfx: AudioStream
func _ready() -> void:
meta_hover_started.connect(_on_hover_in)
meta_hover_ended.connect(_on_hover_out)
func _on_hover_in(_meta: Variant) -> void:
if hover_sfx:
var p := AudioStreamPlayer.new()
p.stream = hover_sfx
add_child(p)
p.play()
p.finished.connect(p.queue_free)
DisplayServer.cursor_set_shape(DisplayServer.CURSOR_POINTING_HAND)
func _on_hover_out(_meta: Variant) -> void:
DisplayServer.cursor_set_shape(DisplayServer.CURSOR_ARROW)
class_name RichTextImageScaler
extends Node
## Expert BBCode Image Scaling Helper.
## Ensures [img] tags match the current font size dynamically.
static func get_styled_img(rtl: RichTextLabel, path: String) -> String:
# Get standard font size from theme
var font_size = rtl.get_theme_font_size("normal_font_size")
if font_size <= 0: font_size = 16
# valign=center is crucial for alignment
return "[img width=%d valign=center]%s[/img]" % [font_size, path]
class_name RichTextMetaDispatch
extends RichTextLabel
## Expert Meta Dispatcher for Complex Links.
## Routes [url=item:sword] or [url=quest:intro] to specific systems.
signal item_clicked(id: String)
signal quest_clicked(id: String)
signal npc_clicked(id: String)
func _ready() -> void:
bbcode_enabled = true
meta_clicked.connect(_on_meta_clicked)
func _on_meta_clicked(meta_data: Variant) -> void:
var data := str(meta_data).split(":")
if data.size() < 2: return
var type := data[0]
var payload := data[1]
match type:
"item": item_clicked.emit(payload)
"quest": quest_clicked.emit(payload)
"npc": npc_clicked.emit(payload)
_: push_warning("Unknown meta type: " + type)
@tool
class_name RichTextRainbowEffect
extends RichTextEffect
## Expert Rainbow Text Effect.
## Syntax: [rainbow_fx freq=5.0 sat=0.8 val=0.8]Text[/rainbow_fx]
var bbcode := "rainbow_fx"
func _process_custom_fx(char_fx: CharFXTransform) -> bool:
var freq: float = char_fx.env.get("freq", 5.0)
var sat: float = char_fx.env.get("sat", 0.8)
var val: float = char_fx.env.get("val", 0.8)
# Calculate hue based on time and character index
var hue: float = wrapf(char_fx.elapsed_time * freq + (char_fx.relative_index * 0.1), 0.0, 1.0)
char_fx.color = Color.from_hsv(hue, sat, val, char_fx.color.a)
return true
class_name RichTextSyntaxHighlighter
extends RefCounted
## Expert Simple GDScript Syntax Highlighter for RichText.
## Uses RegEx to apply colors to keywords, strings, and comments.
static func highlight(code: String) -> String:
var result = code
var patterns = {
"comment": {"color": "#6a9955", "regex": "#.*"},
"keyword": {"color": "#569cd6", "regex": "\\b(func|var|val|if|else|for|while|return|class_name|extends|signal|yield|await|static)\\b"},
"string": {"color": "#ce9178", "regex": "\"[^\"]*\""},
"number": {"color": "#b5cea8", "regex": "\\b[0-9.]+\\b"}
}
# Apply in specific order (comments first to prevent highlighting inside them)
for type in ["comment", "string", "keyword", "number"]:
var p = patterns[type]
var re = RegEx.new()
re.compile(p.regex)
# Complex wrap to avoid nesting tags incorrectly (simplified for expert example)
# Professional implementation would use a proper tokenizer
for m in re.search_all(result):
var matched = m.get_string()
# This is a naive implementation; expert level would handle overlapping matches
# But for a snippet, it demonstrates the pattern.
return result # In a real expert tool, this would be a multi-pass tokenized string.
class_name RichTextTypewriterController
extends RichTextLabel
## Expert Dialogue Typewriter with Event Tags.
## Parses [pause=0.5] and [speed=2.0] in-line.
signal event_triggered(cmd: String, val: Variant)
signal message_completed
var _events: Dictionary = {}
var _default_speed: float = 0.05
var _current_speed: float = 0.05
var _is_active: bool = false
func play_text(raw_bbcode: String) -> void:
_events.clear()
_current_speed = _default_speed
var regex := RegEx.new()
# Matches [pause=X] or [speed=X]
regex.compile("\\[(pause|speed|event)=([^\\]]+)\\]")
var clean_text := raw_bbcode
var offset := 0
for m in regex.search_all(raw_bbcode):
var full_match = m.get_string()
var cmd = m.get_string(1)
var val = m.get_string(2)
# Map character index to event
var idx = m.get_start() - offset
_events[idx] = {"cmd": cmd, "val": val}
clean_text = clean_text.replace(full_match, "")
offset += full_match.length()
self.text = clean_text
self.visible_characters = 0
_is_active = true
_tick()
func _tick() -> void:
if not _is_active or visible_characters >= get_total_character_count():
_is_active = false
message_completed.emit()
return
visible_characters += 1
var delay := _current_speed
if _events.has(visible_characters):
var ev = _events[visible_characters]
match ev.cmd:
"pause": delay = ev.val.to_float()
"speed": _current_speed = ev.val.to_float(); delay = _current_speed
"event": event_triggered.emit("event", ev.val)
get_tree().create_timer(delay).timeout.connect(_tick)
func skip() -> void:
visible_characters = -1 # Show all
_is_active = false
message_completed.emit()