cell height now dynamic in tables
BIN
docs/images/demo_08_bundled_fonts.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
BIN
docs/images/example_07_button_animation.gif
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/images/example_07_pressed_state.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 28 KiB |
BIN
docs/images/functional_elements_demo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
@ -371,6 +371,59 @@ def demo_performance_optimization():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def create_animated_gif():
|
||||||
|
"""
|
||||||
|
Create an animated GIF showing the button press sequence.
|
||||||
|
"""
|
||||||
|
from PIL import Image
|
||||||
|
import os
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("Creating Animated GIF")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check if the PNG files exist
|
||||||
|
png_files = [
|
||||||
|
"demo_07_initial.png",
|
||||||
|
"demo_07_pressed.png",
|
||||||
|
"demo_07_released.png"
|
||||||
|
]
|
||||||
|
|
||||||
|
if not all(os.path.exists(f) for f in png_files):
|
||||||
|
print(" ⚠ PNG files not found, skipping GIF creation")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load the images
|
||||||
|
initial = Image.open('demo_07_initial.png')
|
||||||
|
pressed = Image.open('demo_07_pressed.png')
|
||||||
|
released = Image.open('demo_07_released.png')
|
||||||
|
|
||||||
|
# Create animated GIF showing the button interaction sequence
|
||||||
|
# Sequence: initial (1000ms) -> pressed (200ms) -> released (500ms) -> loop
|
||||||
|
frames = [initial, pressed, released]
|
||||||
|
durations = [1000, 200, 500] # milliseconds per frame
|
||||||
|
|
||||||
|
output_path = "docs/images/example_07_button_animation.gif"
|
||||||
|
|
||||||
|
# Create docs/images directory if it doesn't exist
|
||||||
|
os.makedirs("docs/images", exist_ok=True)
|
||||||
|
|
||||||
|
# Save as animated GIF
|
||||||
|
initial.save(
|
||||||
|
output_path,
|
||||||
|
save_all=True,
|
||||||
|
append_images=[pressed, released],
|
||||||
|
duration=durations,
|
||||||
|
loop=0 # 0 means loop forever
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" ✓ Created: {output_path}")
|
||||||
|
print(f" ✓ Frames: {len(frames)}")
|
||||||
|
print(f" ✓ Sequence: initial (1000ms) → pressed (200ms) → released (500ms)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("\n")
|
print("\n")
|
||||||
print("╔" + "═" * 68 + "╗")
|
print("╔" + "═" * 68 + "╗")
|
||||||
@ -391,6 +444,9 @@ if __name__ == "__main__":
|
|||||||
demo_performance_optimization()
|
demo_performance_optimization()
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
|
# Create animated GIF
|
||||||
|
create_animated_gif()
|
||||||
|
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
print("All demos complete! Check the generated PNG files.")
|
print("All demos complete! Check the generated PNG files and animated GIF.")
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
|
|||||||
@ -68,10 +68,10 @@ Demonstrates:
|
|||||||

|

|
||||||
|
|
||||||
### 05. Tables with Images
|
### 05. Tables with Images
|
||||||
**`05_table_with_images.py`** - Tables containing images and mixed content
|
**`05_html_table_with_images.py`** - Tables containing images and mixed content
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python 05_table_with_images.py
|
python 05_html_table_with_images.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Demonstrates:
|
Demonstrates:
|
||||||
@ -80,8 +80,9 @@ Demonstrates:
|
|||||||
- Book catalog and product showcase tables
|
- Book catalog and product showcase tables
|
||||||
- Mixed content (images and text) in cells
|
- Mixed content (images and text) in cells
|
||||||
- Using cover images from test data
|
- Using cover images from test data
|
||||||
|
- HTML table parsing with `<img>` tags
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 06. Functional Elements (Interactive)
|
### 06. Functional Elements (Interactive)
|
||||||
**`06_functional_elements_demo.py`** - Interactive buttons and forms with callbacks
|
**`06_functional_elements_demo.py`** - Interactive buttons and forms with callbacks
|
||||||
@ -101,13 +102,47 @@ Demonstrates:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### 07. Button Pressed States (Interactive)
|
||||||
|
**`07_pressed_state_demo.py`** - Visual feedback for button interactions
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python 07_pressed_state_demo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Demonstrates:
|
||||||
|
- Button pressed/released state management
|
||||||
|
- Visual feedback timing (150ms press duration)
|
||||||
|
- Automatic interaction handling with `InteractionHandler`
|
||||||
|
- Manual state management for custom event loops
|
||||||
|
- Dirty flag system for optimized re-rendering
|
||||||
|
- State tracking with `InteractionStateManager`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Animated GIF showing button press sequence: initial → pressed → released*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🆕 New Examples (2024-11)
|
## 🆕 New Examples (2024-11)
|
||||||
|
|
||||||
These examples address critical coverage gaps and demonstrate advanced features:
|
These examples address critical coverage gaps and demonstrate advanced features:
|
||||||
|
|
||||||
### 08. Pagination with PageBreak (NEW) ✅
|
### 08. Bundled Fonts Showcase
|
||||||
|
**`08_bundled_fonts_demo.py`** - Demonstration of all bundled fonts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python 08_bundled_fonts_demo.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Demonstrates:
|
||||||
|
- DejaVu Sans (Sans-serif)
|
||||||
|
- DejaVu Serif (Serif)
|
||||||
|
- DejaVu Sans Mono (Monospace)
|
||||||
|
- All font variants: Regular, Bold, Italic, Bold Italic
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 08. Pagination with PageBreak ✅
|
||||||
**`08_pagination_demo.py`** - Multi-page documents with explicit and automatic pagination
|
**`08_pagination_demo.py`** - Multi-page documents with explicit and automatic pagination
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -201,18 +236,6 @@ Demonstrates:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Advanced Examples
|
|
||||||
|
|
||||||
### HTML Rendering
|
|
||||||
|
|
||||||
These examples demonstrate rendering HTML content to multi-page layouts:
|
|
||||||
|
|
||||||
**`html_line_breaking_demo.py`** - Basic HTML line breaking demonstration
|
|
||||||
**`html_multipage_simple.py`** - Simple single-page HTML rendering
|
|
||||||
**`html_multipage_demo_final.py`** - Complete multi-page HTML rendering with headers/footers
|
|
||||||
|
|
||||||
For detailed information about HTML rendering, see `README_HTML_MULTIPAGE.md`.
|
|
||||||
|
|
||||||
## Running the Examples
|
## Running the Examples
|
||||||
|
|
||||||
All examples can be run directly from the examples directory:
|
All examples can be run directly from the examples directory:
|
||||||
@ -220,24 +243,45 @@ All examples can be run directly from the examples directory:
|
|||||||
```bash
|
```bash
|
||||||
cd examples
|
cd examples
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started (01-07)
|
||||||
python 01_simple_page_rendering.py
|
python 01_simple_page_rendering.py # Page layouts
|
||||||
python 02_text_and_layout.py
|
python 02_text_and_layout.py # Text alignment with justified text
|
||||||
python 03_page_layouts.py
|
python 03_page_layouts.py # Various page sizes
|
||||||
python 04_table_rendering.py
|
python 04_table_rendering.py # Table styles
|
||||||
python 05_table_with_images.py
|
python 05_html_table_with_images.py # HTML tables with images
|
||||||
python 06_functional_elements_demo.py
|
python 06_functional_elements_demo.py # Interactive buttons and forms
|
||||||
|
python 07_pressed_state_demo.py # Button pressed states (generates GIF)
|
||||||
|
|
||||||
# NEW: Advanced Features
|
# Advanced Features (08-11)
|
||||||
python 08_pagination_demo.py # Multi-page documents
|
python 08_bundled_fonts_demo.py # Bundled font showcase
|
||||||
python 09_link_navigation_demo.py # All link types
|
python 08_pagination_demo.py # Multi-page documents
|
||||||
python 10_forms_demo.py # All form field types
|
python 09_link_navigation_demo.py # All link types
|
||||||
python 11_table_text_wrapping_demo.py # Table cell text wrapping
|
python 10_forms_demo.py # All form field types
|
||||||
python 11b_simple_table_wrapping.py # Simple wrapping demo
|
python 11_table_text_wrapping_demo.py # Table text wrapping
|
||||||
|
python 11b_simple_table_wrapping.py # Simple wrapping demo
|
||||||
```
|
```
|
||||||
|
|
||||||
Output images are saved to the `docs/images/` directory.
|
Output images are saved to the `docs/images/` directory.
|
||||||
|
|
||||||
|
## Recent Improvements
|
||||||
|
|
||||||
|
### ✅ Justified Text Fix (2024-11-10)
|
||||||
|
Lines using justified alignment now properly fill the entire width by:
|
||||||
|
- Calculating base spacing and remainder pixels
|
||||||
|
- Distributing remainder across word gaps to eliminate short lines
|
||||||
|
- Removing max_spacing constraint for true justification
|
||||||
|
|
||||||
|
**Affected examples:** 02, 11, 11b - All text now perfectly justified!
|
||||||
|
|
||||||
|
### ✅ Animated Button States (2024-11-10)
|
||||||
|
Example 07 now automatically generates an animated GIF showing button interactions:
|
||||||
|
- Initial state (1000ms)
|
||||||
|
- Pressed state (200ms)
|
||||||
|
- Released state (500ms)
|
||||||
|
- Loops continuously
|
||||||
|
|
||||||
|
**Output:** `docs/images/example_07_button_animation.gif`
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
All new examples (08, 09, 10) include comprehensive test coverage:
|
All new examples (08, 09, 10) include comprehensive test coverage:
|
||||||
|
|||||||
@ -472,28 +472,17 @@ class TableRenderer(Box):
|
|||||||
column_width = max(50, available_for_columns // num_columns)
|
column_width = max(50, available_for_columns // num_columns)
|
||||||
column_widths = [column_width] * num_columns
|
column_widths = [column_width] * num_columns
|
||||||
|
|
||||||
# Calculate row heights
|
# Calculate row heights dynamically based on content
|
||||||
# Minimum height needs to account for:
|
header_height = self._calculate_row_height_for_section(
|
||||||
# - Font size (12px) + line height (4px) = 16px per line
|
all_rows, "header", column_widths) if any(
|
||||||
# - Cell padding (varies, but typically 10-20px top+bottom)
|
1 for section, _ in all_rows if section == "header") else 0
|
||||||
# - At least 2 lines of text for wrapping
|
|
||||||
# So minimum should be ~60px to accommodate padding + 2 lines
|
|
||||||
header_height = 60 if any(1 for section,
|
|
||||||
_ in all_rows if section == "header") else 0
|
|
||||||
|
|
||||||
# Check if any body rows contain images - if so, use larger height
|
body_height = self._calculate_row_height_for_section(
|
||||||
body_height = 60 # Increased from 30 to allow for text wrapping
|
all_rows, "body", column_widths)
|
||||||
for section, row in all_rows:
|
|
||||||
if section == "body":
|
|
||||||
for cell in row.cells():
|
|
||||||
for block in cell.blocks():
|
|
||||||
if isinstance(block, AbstractImage):
|
|
||||||
# Use larger height for rows with images
|
|
||||||
body_height = max(body_height, 120)
|
|
||||||
break
|
|
||||||
|
|
||||||
footer_height = 60 if any(1 for section,
|
footer_height = self._calculate_row_height_for_section(
|
||||||
_ in all_rows if section == "footer") else 0
|
all_rows, "footer", column_widths) if any(
|
||||||
|
1 for section, _ in all_rows if section == "footer") else 0
|
||||||
|
|
||||||
row_heights = {
|
row_heights = {
|
||||||
"header": header_height,
|
"header": header_height,
|
||||||
@ -503,6 +492,148 @@ class TableRenderer(Box):
|
|||||||
|
|
||||||
return (column_widths, row_heights)
|
return (column_widths, row_heights)
|
||||||
|
|
||||||
|
def _calculate_row_height_for_section(
|
||||||
|
self,
|
||||||
|
all_rows: List,
|
||||||
|
section: str,
|
||||||
|
column_widths: List[int]) -> int:
|
||||||
|
"""
|
||||||
|
Calculate the maximum required height for rows in a specific section.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_rows: List of all rows in the table
|
||||||
|
section: Section name ('header', 'body', or 'footer')
|
||||||
|
column_widths: List of column widths
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Maximum height needed for rows in this section
|
||||||
|
"""
|
||||||
|
from pyWebLayout.concrete.text import Text
|
||||||
|
from pyWebLayout.style.fonts import Font
|
||||||
|
from pyWebLayout.abstract.inline import Word as AbstractWord
|
||||||
|
|
||||||
|
# Font configuration
|
||||||
|
font_size = 12
|
||||||
|
line_height = font_size + 4
|
||||||
|
padding = self._style.cell_padding
|
||||||
|
vertical_padding = padding[0] + padding[2] # top + bottom
|
||||||
|
horizontal_padding = padding[1] + padding[3] # left + right
|
||||||
|
|
||||||
|
max_height = 40 # Minimum height
|
||||||
|
|
||||||
|
for row_section, row in all_rows:
|
||||||
|
if row_section != section:
|
||||||
|
continue
|
||||||
|
|
||||||
|
row_max_height = 40 # Minimum for this row
|
||||||
|
|
||||||
|
for cell_idx, cell in enumerate(row.cells()):
|
||||||
|
if cell_idx >= len(column_widths):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get cell width (accounting for colspan)
|
||||||
|
cell_width = column_widths[cell_idx]
|
||||||
|
if cell.colspan > 1 and cell_idx + \
|
||||||
|
cell.colspan <= len(column_widths):
|
||||||
|
cell_width = sum(
|
||||||
|
column_widths[cell_idx:cell_idx + cell.colspan])
|
||||||
|
cell_width += self._style.border_width * (cell.colspan - 1)
|
||||||
|
|
||||||
|
# Calculate content width (minus padding)
|
||||||
|
content_width = cell_width - horizontal_padding - 4 # Extra margin
|
||||||
|
|
||||||
|
cell_height = vertical_padding + 4 # Base height with padding
|
||||||
|
|
||||||
|
# Analyze each block in the cell
|
||||||
|
for block in cell.blocks():
|
||||||
|
if isinstance(block, AbstractImage):
|
||||||
|
# Images need more space
|
||||||
|
cell_height = max(cell_height, 120)
|
||||||
|
elif isinstance(block, (Paragraph, Heading)):
|
||||||
|
# Calculate text wrapping height
|
||||||
|
word_items = block.words() if callable(
|
||||||
|
block.words) else block.words
|
||||||
|
words = list(word_items)
|
||||||
|
|
||||||
|
if not words:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Simulate text wrapping to count lines
|
||||||
|
lines_needed = self._estimate_wrapped_lines(
|
||||||
|
words, content_width, font_size)
|
||||||
|
text_height = lines_needed * line_height
|
||||||
|
cell_height = max(
|
||||||
|
cell_height, text_height + vertical_padding + 4)
|
||||||
|
|
||||||
|
row_max_height = max(row_max_height, cell_height)
|
||||||
|
|
||||||
|
max_height = max(max_height, row_max_height)
|
||||||
|
|
||||||
|
return max_height
|
||||||
|
|
||||||
|
def _estimate_wrapped_lines(
|
||||||
|
self,
|
||||||
|
words: List,
|
||||||
|
available_width: int,
|
||||||
|
font_size: int) -> int:
|
||||||
|
"""
|
||||||
|
Estimate how many lines are needed to render the given words.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
words: List of word objects
|
||||||
|
available_width: Available width for text
|
||||||
|
font_size: Font size in pixels
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of lines needed
|
||||||
|
"""
|
||||||
|
from pyWebLayout.concrete.text import Text
|
||||||
|
from pyWebLayout.style.fonts import Font
|
||||||
|
|
||||||
|
# Create a temporary font for measurement
|
||||||
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||||
|
font = Font(font_path=font_path, font_size=font_size)
|
||||||
|
|
||||||
|
# Word spacing (approximate)
|
||||||
|
word_spacing = int(font_size * 0.25)
|
||||||
|
|
||||||
|
lines = 1
|
||||||
|
current_line_width = 0
|
||||||
|
|
||||||
|
for word_item in words:
|
||||||
|
# Handle word tuples (index, word_obj)
|
||||||
|
if isinstance(word_item, tuple) and len(word_item) >= 2:
|
||||||
|
word_obj = word_item[1]
|
||||||
|
else:
|
||||||
|
word_obj = word_item
|
||||||
|
|
||||||
|
# Extract text from the word
|
||||||
|
word_text = word_obj.text if hasattr(
|
||||||
|
word_obj, 'text') else str(word_obj)
|
||||||
|
|
||||||
|
# Measure word width
|
||||||
|
word_width = font.font.getlength(word_text)
|
||||||
|
|
||||||
|
# Check if word fits on current line
|
||||||
|
if current_line_width > 0: # Not first word on line
|
||||||
|
needed_width = current_line_width + word_spacing + word_width
|
||||||
|
if needed_width > available_width:
|
||||||
|
# Need new line
|
||||||
|
lines += 1
|
||||||
|
current_line_width = word_width
|
||||||
|
else:
|
||||||
|
current_line_width = needed_width
|
||||||
|
else:
|
||||||
|
# First word on line
|
||||||
|
if word_width > available_width:
|
||||||
|
# Word needs to be hyphenated, assume it takes 1 line
|
||||||
|
lines += 1
|
||||||
|
current_line_width = 0
|
||||||
|
else:
|
||||||
|
current_line_width = word_width
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
def render(self) -> Image.Image:
|
def render(self) -> Image.Image:
|
||||||
"""Render the complete table."""
|
"""Render the complete table."""
|
||||||
x, y = self._origin
|
x, y = self._origin
|
||||||
|
|||||||
@ -131,24 +131,45 @@ class CenterRightAlignmentHandler(AlignmentHandler):
|
|||||||
class JustifyAlignmentHandler(AlignmentHandler):
|
class JustifyAlignmentHandler(AlignmentHandler):
|
||||||
"""Handler for justified text with full justification."""
|
"""Handler for justified text with full justification."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# Store variable spacing for each gap to distribute remainder pixels
|
||||||
|
self._gap_spacings: List[int] = []
|
||||||
|
|
||||||
def calculate_spacing_and_position(self, text_objects: List['Text'],
|
def calculate_spacing_and_position(self, text_objects: List['Text'],
|
||||||
available_width: int, min_spacing: int,
|
available_width: int, min_spacing: int,
|
||||||
max_spacing: int) -> Tuple[int, int, bool]:
|
max_spacing: int) -> Tuple[int, int, bool]:
|
||||||
"""Justified alignment distributes space to fill the entire line width."""
|
"""
|
||||||
|
Justified alignment distributes space to fill the entire line width.
|
||||||
|
|
||||||
|
For justified text, we ALWAYS try to fill the entire width by distributing
|
||||||
|
space between words, regardless of max_spacing constraints. The only limit
|
||||||
|
is min_spacing to ensure readability.
|
||||||
|
"""
|
||||||
|
|
||||||
word_length = sum([word.width for word in text_objects])
|
word_length = sum([word.width for word in text_objects])
|
||||||
residual_space = available_width - word_length
|
residual_space = available_width - word_length
|
||||||
num_gaps = max(1, len(text_objects) - 1)
|
num_gaps = max(1, len(text_objects) - 1)
|
||||||
|
|
||||||
actual_spacing = residual_space // num_gaps
|
# For justified text, calculate the actual spacing needed to fill the line
|
||||||
ideal_space = (min_spacing + max_spacing) // 2
|
base_spacing = int(residual_space // num_gaps)
|
||||||
# can we touch the end?
|
remainder = int(residual_space % num_gaps) # The extra pixels to distribute
|
||||||
if actual_spacing < max_spacing:
|
|
||||||
if actual_spacing < min_spacing:
|
# Check if we have enough space for minimum spacing
|
||||||
# Ensure we never return spacing less than min_spacing
|
if base_spacing < min_spacing:
|
||||||
return min_spacing, 0, True
|
# Not enough space - this is overflow
|
||||||
return max(min_spacing, actual_spacing), 0, False
|
self._gap_spacings = [min_spacing] * num_gaps
|
||||||
return ideal_space, 0, False
|
return min_spacing, 0, True
|
||||||
|
|
||||||
|
# Distribute remainder pixels across the first 'remainder' gaps
|
||||||
|
# This ensures the line fills the entire width exactly
|
||||||
|
self._gap_spacings = []
|
||||||
|
for i in range(num_gaps):
|
||||||
|
if i < remainder:
|
||||||
|
self._gap_spacings.append(base_spacing + 1)
|
||||||
|
else:
|
||||||
|
self._gap_spacings.append(base_spacing)
|
||||||
|
|
||||||
|
return base_spacing, 0, False
|
||||||
|
|
||||||
|
|
||||||
class Text(Renderable, Queriable):
|
class Text(Renderable, Queriable):
|
||||||
@ -642,12 +663,20 @@ class Line(Box):
|
|||||||
next_text = self._text_objects[i + 1] if i + \
|
next_text = self._text_objects[i + 1] if i + \
|
||||||
1 < len(self._text_objects) else None
|
1 < len(self._text_objects) else None
|
||||||
|
|
||||||
|
# Get the spacing for this specific gap (variable for justified text)
|
||||||
|
if isinstance(self._alignment_handler, JustifyAlignmentHandler) and \
|
||||||
|
hasattr(self._alignment_handler, '_gap_spacings') and \
|
||||||
|
i < len(self._alignment_handler._gap_spacings):
|
||||||
|
current_spacing = self._alignment_handler._gap_spacings[i]
|
||||||
|
else:
|
||||||
|
current_spacing = self._spacing_render
|
||||||
|
|
||||||
# Render with next text information for continuous underline/strikethrough
|
# Render with next text information for continuous underline/strikethrough
|
||||||
text.render(next_text, self._spacing_render)
|
text.render(next_text, current_spacing)
|
||||||
# Add text width, then spacing only if there are more words
|
# Add text width, then spacing only if there are more words
|
||||||
x_cursor += text.width
|
x_cursor += text.width
|
||||||
if i < len(self._text_objects) - 1:
|
if i < len(self._text_objects) - 1:
|
||||||
x_cursor += self._spacing_render
|
x_cursor += current_spacing
|
||||||
|
|
||||||
def query_point(self, point: Tuple[int, int]) -> Optional['QueryResult']:
|
def query_point(self, point: Tuple[int, int]) -> Optional['QueryResult']:
|
||||||
"""
|
"""
|
||||||
|
|||||||