This commit is contained in:
parent
dfa225fdcb
commit
e6c17ef8a8
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user