286 lines
8.7 KiB
Python
286 lines
8.7 KiB
Python
|
|
# McRogueFace Cookbook - Text Box Widget
|
||
|
|
"""
|
||
|
|
Word-wrapped text display with typewriter effect.
|
||
|
|
|
||
|
|
Example:
|
||
|
|
from lib.text_box import TextBox
|
||
|
|
|
||
|
|
text_box = TextBox(
|
||
|
|
pos=(100, 100),
|
||
|
|
size=(400, 200),
|
||
|
|
text="This is a long text that will be word-wrapped...",
|
||
|
|
chars_per_second=30
|
||
|
|
)
|
||
|
|
scene.children.append(text_box.frame)
|
||
|
|
|
||
|
|
# Skip animation
|
||
|
|
text_box.skip_animation()
|
||
|
|
"""
|
||
|
|
import mcrfpy
|
||
|
|
|
||
|
|
|
||
|
|
class TextBox:
|
||
|
|
"""Word-wrapped text with optional typewriter effect.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
pos: (x, y) position tuple
|
||
|
|
size: (width, height) tuple
|
||
|
|
text: Initial text to display
|
||
|
|
chars_per_second: Typewriter speed (0 = instant)
|
||
|
|
font_size: Text size (default: 14)
|
||
|
|
text_color: Color of text (default: white)
|
||
|
|
bg_color: Background color (default: dark)
|
||
|
|
outline_color: Border color (default: gray)
|
||
|
|
outline: Border thickness (default: 1)
|
||
|
|
padding: Internal padding (default: 10)
|
||
|
|
|
||
|
|
Attributes:
|
||
|
|
frame: The outer frame (add this to scene)
|
||
|
|
text: Current text content
|
||
|
|
is_complete: Whether typewriter animation finished
|
||
|
|
"""
|
||
|
|
|
||
|
|
DEFAULT_TEXT_COLOR = mcrfpy.Color(220, 220, 220)
|
||
|
|
DEFAULT_BG_COLOR = mcrfpy.Color(25, 25, 30)
|
||
|
|
DEFAULT_OUTLINE = mcrfpy.Color(80, 80, 90)
|
||
|
|
|
||
|
|
def __init__(self, pos, size, text="", chars_per_second=30,
|
||
|
|
font_size=14, text_color=None, bg_color=None,
|
||
|
|
outline_color=None, outline=1, padding=10):
|
||
|
|
self.pos = pos
|
||
|
|
self.size = size
|
||
|
|
self._full_text = text
|
||
|
|
self.chars_per_second = chars_per_second
|
||
|
|
self.font_size = font_size
|
||
|
|
self.padding = padding
|
||
|
|
|
||
|
|
# Colors
|
||
|
|
self.text_color = text_color or self.DEFAULT_TEXT_COLOR
|
||
|
|
self.bg_color = bg_color or self.DEFAULT_BG_COLOR
|
||
|
|
self.outline_color = outline_color or self.DEFAULT_OUTLINE
|
||
|
|
|
||
|
|
# State
|
||
|
|
self._displayed_chars = 0
|
||
|
|
self._is_complete = chars_per_second == 0
|
||
|
|
self._on_complete = None
|
||
|
|
self._timer_name = None
|
||
|
|
|
||
|
|
# Create outer frame
|
||
|
|
self.frame = mcrfpy.Frame(
|
||
|
|
pos=pos,
|
||
|
|
size=size,
|
||
|
|
fill_color=self.bg_color,
|
||
|
|
outline_color=self.outline_color,
|
||
|
|
outline=outline
|
||
|
|
)
|
||
|
|
|
||
|
|
# Calculate text area
|
||
|
|
self._text_width = size[0] - padding * 2
|
||
|
|
self._text_height = size[1] - padding * 2
|
||
|
|
|
||
|
|
# Create text caption
|
||
|
|
self._caption = mcrfpy.Caption(
|
||
|
|
text="",
|
||
|
|
pos=(padding, padding),
|
||
|
|
fill_color=self.text_color,
|
||
|
|
font_size=font_size
|
||
|
|
)
|
||
|
|
self.frame.children.append(self._caption)
|
||
|
|
|
||
|
|
# Start typewriter if there's text
|
||
|
|
if text and chars_per_second > 0:
|
||
|
|
self._start_typewriter()
|
||
|
|
elif text:
|
||
|
|
self._caption.text = self._word_wrap(text)
|
||
|
|
self._displayed_chars = len(text)
|
||
|
|
self._is_complete = True
|
||
|
|
|
||
|
|
def _word_wrap(self, text):
|
||
|
|
"""Word-wrap text to fit within the text box width.
|
||
|
|
|
||
|
|
Simple implementation that breaks on spaces.
|
||
|
|
"""
|
||
|
|
# Estimate chars per line based on font size
|
||
|
|
# This is approximate - real implementation would measure text
|
||
|
|
avg_char_width = self.font_size * 0.6
|
||
|
|
chars_per_line = int(self._text_width / avg_char_width)
|
||
|
|
|
||
|
|
if chars_per_line <= 0:
|
||
|
|
return text
|
||
|
|
|
||
|
|
words = text.split(' ')
|
||
|
|
lines = []
|
||
|
|
current_line = []
|
||
|
|
current_length = 0
|
||
|
|
|
||
|
|
for word in words:
|
||
|
|
# Check if word fits on current line
|
||
|
|
word_len = len(word)
|
||
|
|
if current_length + word_len + (1 if current_line else 0) <= chars_per_line:
|
||
|
|
current_line.append(word)
|
||
|
|
current_length += word_len + (1 if len(current_line) > 1 else 0)
|
||
|
|
else:
|
||
|
|
# Start new line
|
||
|
|
if current_line:
|
||
|
|
lines.append(' '.join(current_line))
|
||
|
|
current_line = [word]
|
||
|
|
current_length = word_len
|
||
|
|
|
||
|
|
# Add last line
|
||
|
|
if current_line:
|
||
|
|
lines.append(' '.join(current_line))
|
||
|
|
|
||
|
|
return '\n'.join(lines)
|
||
|
|
|
||
|
|
def _start_typewriter(self):
|
||
|
|
"""Start the typewriter animation."""
|
||
|
|
if self.chars_per_second <= 0:
|
||
|
|
return
|
||
|
|
|
||
|
|
self._displayed_chars = 0
|
||
|
|
self._is_complete = False
|
||
|
|
|
||
|
|
# Calculate interval in milliseconds
|
||
|
|
interval_ms = max(1, int(1000 / self.chars_per_second))
|
||
|
|
|
||
|
|
self._timer_name = f"textbox_{id(self)}"
|
||
|
|
mcrfpy.Timer(self._timer_name, self._typewriter_tick, interval_ms)
|
||
|
|
|
||
|
|
def _typewriter_tick(self, runtime):
|
||
|
|
"""Add one character to the display."""
|
||
|
|
if self._displayed_chars >= len(self._full_text):
|
||
|
|
# Animation complete
|
||
|
|
self._is_complete = True
|
||
|
|
# Stop timer by deleting it - there's no direct stop method
|
||
|
|
# The timer will continue but we'll just not update
|
||
|
|
if self._on_complete:
|
||
|
|
self._on_complete()
|
||
|
|
return
|
||
|
|
|
||
|
|
self._displayed_chars += 1
|
||
|
|
visible_text = self._full_text[:self._displayed_chars]
|
||
|
|
self._caption.text = self._word_wrap(visible_text)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def text(self):
|
||
|
|
"""Get the full text content."""
|
||
|
|
return self._full_text
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_complete(self):
|
||
|
|
"""Whether the typewriter animation has finished."""
|
||
|
|
return self._is_complete
|
||
|
|
|
||
|
|
@property
|
||
|
|
def on_complete(self):
|
||
|
|
"""Callback when animation completes."""
|
||
|
|
return self._on_complete
|
||
|
|
|
||
|
|
@on_complete.setter
|
||
|
|
def on_complete(self, callback):
|
||
|
|
"""Set callback for animation completion."""
|
||
|
|
self._on_complete = callback
|
||
|
|
|
||
|
|
def set_text(self, text, animate=True):
|
||
|
|
"""Change the text content.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
text: New text to display
|
||
|
|
animate: Whether to use typewriter effect
|
||
|
|
"""
|
||
|
|
self._full_text = text
|
||
|
|
|
||
|
|
if animate and self.chars_per_second > 0:
|
||
|
|
self._start_typewriter()
|
||
|
|
else:
|
||
|
|
self._caption.text = self._word_wrap(text)
|
||
|
|
self._displayed_chars = len(text)
|
||
|
|
self._is_complete = True
|
||
|
|
|
||
|
|
def skip_animation(self):
|
||
|
|
"""Skip to the end of the typewriter animation."""
|
||
|
|
self._displayed_chars = len(self._full_text)
|
||
|
|
self._caption.text = self._word_wrap(self._full_text)
|
||
|
|
self._is_complete = True
|
||
|
|
if self._on_complete:
|
||
|
|
self._on_complete()
|
||
|
|
|
||
|
|
def clear(self):
|
||
|
|
"""Clear the text box."""
|
||
|
|
self._full_text = ""
|
||
|
|
self._displayed_chars = 0
|
||
|
|
self._caption.text = ""
|
||
|
|
self._is_complete = True
|
||
|
|
|
||
|
|
def append_text(self, text, animate=True):
|
||
|
|
"""Append text to the current content.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
text: Text to append
|
||
|
|
animate: Whether to animate the new text
|
||
|
|
"""
|
||
|
|
new_text = self._full_text + text
|
||
|
|
self.set_text(new_text, animate=animate)
|
||
|
|
|
||
|
|
|
||
|
|
class DialogueBox(TextBox):
|
||
|
|
"""Specialized text box for dialogue with speaker name.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
pos: (x, y) position tuple
|
||
|
|
size: (width, height) tuple
|
||
|
|
speaker: Name of the speaker
|
||
|
|
text: Dialogue text
|
||
|
|
speaker_color: Color for speaker name (default: yellow)
|
||
|
|
**kwargs: Additional TextBox arguments
|
||
|
|
"""
|
||
|
|
|
||
|
|
DEFAULT_SPEAKER_COLOR = mcrfpy.Color(255, 220, 100)
|
||
|
|
|
||
|
|
def __init__(self, pos, size, speaker="", text="",
|
||
|
|
speaker_color=None, **kwargs):
|
||
|
|
# Initialize parent with empty text
|
||
|
|
super().__init__(pos, size, text="", **kwargs)
|
||
|
|
|
||
|
|
self._speaker = speaker
|
||
|
|
self.speaker_color = speaker_color or self.DEFAULT_SPEAKER_COLOR
|
||
|
|
|
||
|
|
# Create speaker name caption
|
||
|
|
self._speaker_caption = mcrfpy.Caption(
|
||
|
|
text=speaker,
|
||
|
|
pos=(self.padding, self.padding - 5),
|
||
|
|
fill_color=self.speaker_color,
|
||
|
|
font_size=self.font_size + 2
|
||
|
|
)
|
||
|
|
self._speaker_caption.outline = 1
|
||
|
|
self._speaker_caption.outline_color = mcrfpy.Color(0, 0, 0)
|
||
|
|
self.frame.children.append(self._speaker_caption)
|
||
|
|
|
||
|
|
# Adjust main text position
|
||
|
|
self._caption.y = self.padding + self.font_size + 10
|
||
|
|
|
||
|
|
# Now set the actual text
|
||
|
|
if text:
|
||
|
|
self.set_text(text, animate=kwargs.get('chars_per_second', 30) > 0)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def speaker(self):
|
||
|
|
"""Get the speaker name."""
|
||
|
|
return self._speaker
|
||
|
|
|
||
|
|
@speaker.setter
|
||
|
|
def speaker(self, value):
|
||
|
|
"""Set the speaker name."""
|
||
|
|
self._speaker = value
|
||
|
|
self._speaker_caption.text = value
|
||
|
|
|
||
|
|
def set_dialogue(self, speaker, text, animate=True):
|
||
|
|
"""Set both speaker and text.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
speaker: Speaker name
|
||
|
|
text: Dialogue text
|
||
|
|
animate: Whether to animate the text
|
||
|
|
"""
|
||
|
|
self.speaker = speaker
|
||
|
|
self.set_text(text, animate=animate)
|