updating viewport
All checks were successful
Python CI / test (push) Successful in 7m51s

This commit is contained in:
Duncan Tourolle 2025-11-04 23:34:23 +01:00
parent dfa225fdcb
commit e6c17ef8a8
5 changed files with 108 additions and 42 deletions

View File

@ -43,6 +43,24 @@ python simple_ereader_example.py tests/data/test.epub
python ereader_demo.py tests/data/test.epub
```
## Browser Examples
### HTML Browser with Viewport System
**`html_browser_with_viewport.py`** (located in `pyWebLayout/examples/`) - Full-featured HTML browser using the viewport system:
```bash
python pyWebLayout/examples/html_browser_with_viewport.py
```
This demonstrates:
- Viewport-based scrolling (mouse wheel, keyboard, scrollbar)
- Efficient rendering of large documents
- Text selection and clipboard support
- Navigation and history management
- Interactive HTML viewing
For detailed information about the viewport system, see `README_BROWSER_VIEWPORT.md`.
## Other Examples
### HTML Rendering
@ -60,6 +78,7 @@ For detailed information about HTML rendering, see `README_HTML_MULTIPAGE.md`.
- `README_EREADER.md` - Detailed EbookReader API documentation
- `README_HTML_MULTIPAGE.md` - HTML multi-page rendering guide
- `README_BROWSER_VIEWPORT.md` - Browser viewport system documentation
- `pyWebLayout/layout/README_EREADER_API.md` - EbookReader API reference (in source)
## Debug/Development Scripts

View File

@ -3,3 +3,4 @@ from .page import Page
from .text import Text, Line
from .functional import LinkText, ButtonText, FormFieldText, create_link_text, create_button_text, create_form_field_text
from .image import RenderableImage
from .viewport import Viewport, ScrollablePageContent

View File

@ -40,7 +40,8 @@ class Viewport(Box, Layoutable):
# Viewport position within the content (scroll offset)
self._viewport_offset = np.array([0, 0])
# Content list to hold all the content (since Box doesn't have add_child)
self._content_items = []
# Cached content bounds for optimization
self._content_bounds_cache = None
@ -82,37 +83,33 @@ class Viewport(Box, Layoutable):
def add_content(self, renderable: Renderable) -> 'Viewport':
"""Add content to the viewport's content area"""
self._content_container.add_child(renderable)
self._content_items.append(renderable)
self._cache_dirty = True
return self
def clear_content(self) -> 'Viewport':
"""Clear all content from the viewport"""
self._content_container._children.clear()
self._content_items.clear()
self._cache_dirty = True
return self
def set_content_size(self, size: Tuple[int, int]) -> 'Viewport':
"""Set the total content size explicitly"""
self._content_size = np.array(size)
self._content_container._size = self._content_size
self._cache_dirty = True
return self
def _update_content_size(self):
"""Auto-calculate content size from children"""
if not self._content_container._children:
if not self._content_items:
self._content_size = self._viewport_size.copy()
return
# Layout children to get their positions
self._content_container.layout()
# Find the bounds of all children
max_x = 0
max_y = 0
for child in self._content_container._children:
for child in self._content_items:
if hasattr(child, '_origin') and hasattr(child, '_size'):
child_origin = np.array(child._origin)
child_size = np.array(child._size)
@ -127,15 +124,14 @@ class Viewport(Box, Layoutable):
max(max_y, self._viewport_size[1])
])
self._content_container._size = self._content_size
def _get_content_bounds(self) -> List[Tuple]:
"""Get bounds of all content elements for efficient intersection testing"""
if not self._cache_dirty and self._content_bounds_cache is not None:
return self._content_bounds_cache
bounds = []
self._collect_element_bounds(self._content_container, np.array([0, 0]), bounds)
for item in self._content_items:
self._collect_element_bounds(item, np.array([0, 0]), bounds)
self._content_bounds_cache = bounds
self._cache_dirty = False
@ -280,8 +276,11 @@ class Viewport(Box, Layoutable):
if self._content_size is None:
self._update_content_size()
# Layout all content
self._content_container.layout()
# Layout all content items
for item in self._content_items:
if hasattr(item, 'layout'):
item.layout()
self._cache_dirty = True
def render(self) -> Image.Image:
@ -393,33 +392,39 @@ class ScrollablePageContent(Box):
This extends the regular Box functionality but allows for much larger content areas.
"""
def __init__(self, content_width: int = 800, initial_height: int = 1000,
direction='vertical', spacing=10, padding=(0, 0, 0, 0)):
def __init__(self, content_width: int = 800, initial_height: int = 1000):
"""
Initialize scrollable page content.
Args:
content_width: Width of the content area
initial_height: Initial height (will grow as content is added)
direction: Layout direction
spacing: Spacing between elements
padding: Padding around content (no padding to avoid viewport clipping issues)
"""
super().__init__(
origin=(0, 0),
size=(content_width, initial_height),
direction=direction,
spacing=spacing,
padding=padding # No padding to avoid any positioning issues with viewport
size=(content_width, initial_height)
)
self._content_width = content_width
self._auto_height = True
self._children = []
self._spacing = 10
self._current_y = 0
def add_child(self, child: Renderable):
"""Add a child and update content height if needed"""
super().add_child(child)
# Add child to the list
self._children.append(child)
# Position the child vertically
if hasattr(child, '_origin'):
child._origin = np.array([0, self._current_y])
# Update current Y position for next child
if hasattr(child, '_size'):
self._current_y += child._size[1] + self._spacing
# Update content height if needed
if self._auto_height:
self._update_content_height()
@ -430,9 +435,6 @@ class ScrollablePageContent(Box):
if not self._children:
return
# Layout children to get accurate positions
super().layout()
# Find the bottom-most child
max_bottom = 0
for child in self._children:
@ -440,13 +442,37 @@ class ScrollablePageContent(Box):
child_bottom = child._origin[1] + child._size[1]
max_bottom = max(max_bottom, child_bottom)
# Add some bottom padding
new_height = max_bottom + self._padding[2] + self._spacing
# Add some bottom spacing
new_height = max_bottom + self._spacing
# Update size if needed
if new_height > self._size[1]:
self._size = np.array([self._content_width, new_height])
def layout(self):
"""Layout children (already positioned in add_child)"""
# Children are already positioned, just update height
self._update_content_height()
def render(self) -> Image.Image:
"""Render all children onto the content area"""
canvas = Image.new('RGBA', tuple(self._size), (255, 255, 255, 0))
for child in self._children:
try:
child_img = child.render()
if hasattr(child, '_origin'):
pos = tuple(child._origin.astype(int))
if child_img.mode == 'RGBA':
canvas.paste(child_img, pos, child_img)
else:
canvas.paste(child_img, pos)
except Exception:
# Skip children that fail to render
continue
return canvas
def get_content_height(self) -> int:
"""Get the total content height"""
self._update_content_height()

View File

@ -20,8 +20,8 @@ import pyperclip
# Import pyWebLayout components including the new viewport system
from pyWebLayout.concrete import (
Page, Box, Text, RenderableImage,
RenderableLink, RenderableButton, RenderableForm, RenderableFormField,
Page, Box, Text, RenderableImage,
LinkText, ButtonText, FormFieldText,
Viewport, ScrollablePageContent
)
from pyWebLayout.abstract.functional import (
@ -53,17 +53,37 @@ class HTMLViewportAdapter:
# Create scrollable content container
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
# Convert abstract blocks to renderable objects using Page's conversion system
page = Page(size=(viewport_size[0], 10000)) # Large temporary page
# Create simple text elements from the blocks
try:
from PIL import ImageDraw
# Add blocks to page and let it handle the conversion
for i, block in enumerate(blocks):
renderable = page._convert_block_to_renderable(block)
if renderable:
content.add_child(renderable)
# Add spacing between blocks (but not after the last block)
if i < len(blocks) - 1:
content.add_child(Box((0, 0), (1, 8)))
temp_img = Image.new('RGB', (1, 1))
draw = ImageDraw.Draw(temp_img)
for i, block in enumerate(blocks):
# Create simple text representation
if isinstance(block, Paragraph):
# Extract text from words in the paragraph
text_parts = []
for inline in block.inlines:
if isinstance(inline, Word):
text_parts.append(inline.text)
if text_parts:
text_content = " ".join(text_parts)
text_elem = Text(text_content, base_font, draw)
content.add_child(text_elem)
# Add spacing between blocks
if i < len(blocks) - 1:
content.add_child(Box((0, 0), (1, 8)))
except Exception as e:
# If rendering fails, add error message
temp_img = Image.new('RGB', (1, 1))
draw = ImageDraw.Draw(temp_img)
error_text = Text(f"Error rendering content: {str(e)}",
Font(font_size=14, colour=(255, 0, 0)), draw)
content.add_child(error_text)
# Create viewport and add the content
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))