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
|
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
|
## Other Examples
|
||||||
|
|
||||||
### HTML Rendering
|
### 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_EREADER.md` - Detailed EbookReader API documentation
|
||||||
- `README_HTML_MULTIPAGE.md` - HTML multi-page rendering guide
|
- `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)
|
- `pyWebLayout/layout/README_EREADER_API.md` - EbookReader API reference (in source)
|
||||||
|
|
||||||
## Debug/Development Scripts
|
## Debug/Development Scripts
|
||||||
|
|||||||
@ -3,3 +3,4 @@ from .page import Page
|
|||||||
from .text import Text, Line
|
from .text import Text, Line
|
||||||
from .functional import LinkText, ButtonText, FormFieldText, create_link_text, create_button_text, create_form_field_text
|
from .functional import LinkText, ButtonText, FormFieldText, create_link_text, create_button_text, create_form_field_text
|
||||||
from .image import RenderableImage
|
from .image import RenderableImage
|
||||||
|
from .viewport import Viewport, ScrollablePageContent
|
||||||
|
|||||||
@ -40,7 +40,8 @@ class Viewport(Box, Layoutable):
|
|||||||
# Viewport position within the content (scroll offset)
|
# Viewport position within the content (scroll offset)
|
||||||
self._viewport_offset = np.array([0, 0])
|
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
|
# Cached content bounds for optimization
|
||||||
self._content_bounds_cache = None
|
self._content_bounds_cache = None
|
||||||
@ -82,37 +83,33 @@ class Viewport(Box, Layoutable):
|
|||||||
|
|
||||||
def add_content(self, renderable: Renderable) -> 'Viewport':
|
def add_content(self, renderable: Renderable) -> 'Viewport':
|
||||||
"""Add content to the viewport's content area"""
|
"""Add content to the viewport's content area"""
|
||||||
self._content_container.add_child(renderable)
|
self._content_items.append(renderable)
|
||||||
self._cache_dirty = True
|
self._cache_dirty = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def clear_content(self) -> 'Viewport':
|
def clear_content(self) -> 'Viewport':
|
||||||
"""Clear all content from the viewport"""
|
"""Clear all content from the viewport"""
|
||||||
self._content_container._children.clear()
|
self._content_items.clear()
|
||||||
self._cache_dirty = True
|
self._cache_dirty = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def set_content_size(self, size: Tuple[int, int]) -> 'Viewport':
|
def set_content_size(self, size: Tuple[int, int]) -> 'Viewport':
|
||||||
"""Set the total content size explicitly"""
|
"""Set the total content size explicitly"""
|
||||||
self._content_size = np.array(size)
|
self._content_size = np.array(size)
|
||||||
self._content_container._size = self._content_size
|
|
||||||
self._cache_dirty = True
|
self._cache_dirty = True
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _update_content_size(self):
|
def _update_content_size(self):
|
||||||
"""Auto-calculate content size from children"""
|
"""Auto-calculate content size from children"""
|
||||||
if not self._content_container._children:
|
if not self._content_items:
|
||||||
self._content_size = self._viewport_size.copy()
|
self._content_size = self._viewport_size.copy()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Layout children to get their positions
|
|
||||||
self._content_container.layout()
|
|
||||||
|
|
||||||
# Find the bounds of all children
|
# Find the bounds of all children
|
||||||
max_x = 0
|
max_x = 0
|
||||||
max_y = 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'):
|
if hasattr(child, '_origin') and hasattr(child, '_size'):
|
||||||
child_origin = np.array(child._origin)
|
child_origin = np.array(child._origin)
|
||||||
child_size = np.array(child._size)
|
child_size = np.array(child._size)
|
||||||
@ -127,15 +124,14 @@ class Viewport(Box, Layoutable):
|
|||||||
max(max_y, self._viewport_size[1])
|
max(max_y, self._viewport_size[1])
|
||||||
])
|
])
|
||||||
|
|
||||||
self._content_container._size = self._content_size
|
|
||||||
|
|
||||||
def _get_content_bounds(self) -> List[Tuple]:
|
def _get_content_bounds(self) -> List[Tuple]:
|
||||||
"""Get bounds of all content elements for efficient intersection testing"""
|
"""Get bounds of all content elements for efficient intersection testing"""
|
||||||
if not self._cache_dirty and self._content_bounds_cache is not None:
|
if not self._cache_dirty and self._content_bounds_cache is not None:
|
||||||
return self._content_bounds_cache
|
return self._content_bounds_cache
|
||||||
|
|
||||||
bounds = []
|
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._content_bounds_cache = bounds
|
||||||
self._cache_dirty = False
|
self._cache_dirty = False
|
||||||
@ -280,8 +276,11 @@ class Viewport(Box, Layoutable):
|
|||||||
if self._content_size is None:
|
if self._content_size is None:
|
||||||
self._update_content_size()
|
self._update_content_size()
|
||||||
|
|
||||||
# Layout all content
|
# Layout all content items
|
||||||
self._content_container.layout()
|
for item in self._content_items:
|
||||||
|
if hasattr(item, 'layout'):
|
||||||
|
item.layout()
|
||||||
|
|
||||||
self._cache_dirty = True
|
self._cache_dirty = True
|
||||||
|
|
||||||
def render(self) -> Image.Image:
|
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.
|
This extends the regular Box functionality but allows for much larger content areas.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, content_width: int = 800, initial_height: int = 1000,
|
def __init__(self, content_width: int = 800, initial_height: int = 1000):
|
||||||
direction='vertical', spacing=10, padding=(0, 0, 0, 0)):
|
|
||||||
"""
|
"""
|
||||||
Initialize scrollable page content.
|
Initialize scrollable page content.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
content_width: Width of the content area
|
content_width: Width of the content area
|
||||||
initial_height: Initial height (will grow as content is added)
|
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__(
|
super().__init__(
|
||||||
origin=(0, 0),
|
origin=(0, 0),
|
||||||
size=(content_width, initial_height),
|
size=(content_width, initial_height)
|
||||||
direction=direction,
|
|
||||||
spacing=spacing,
|
|
||||||
padding=padding # No padding to avoid any positioning issues with viewport
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._content_width = content_width
|
self._content_width = content_width
|
||||||
self._auto_height = True
|
self._auto_height = True
|
||||||
|
self._children = []
|
||||||
|
self._spacing = 10
|
||||||
|
self._current_y = 0
|
||||||
|
|
||||||
def add_child(self, child: Renderable):
|
def add_child(self, child: Renderable):
|
||||||
"""Add a child and update content height if needed"""
|
"""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:
|
if self._auto_height:
|
||||||
self._update_content_height()
|
self._update_content_height()
|
||||||
|
|
||||||
@ -430,9 +435,6 @@ class ScrollablePageContent(Box):
|
|||||||
if not self._children:
|
if not self._children:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Layout children to get accurate positions
|
|
||||||
super().layout()
|
|
||||||
|
|
||||||
# Find the bottom-most child
|
# Find the bottom-most child
|
||||||
max_bottom = 0
|
max_bottom = 0
|
||||||
for child in self._children:
|
for child in self._children:
|
||||||
@ -440,13 +442,37 @@ class ScrollablePageContent(Box):
|
|||||||
child_bottom = child._origin[1] + child._size[1]
|
child_bottom = child._origin[1] + child._size[1]
|
||||||
max_bottom = max(max_bottom, child_bottom)
|
max_bottom = max(max_bottom, child_bottom)
|
||||||
|
|
||||||
# Add some bottom padding
|
# Add some bottom spacing
|
||||||
new_height = max_bottom + self._padding[2] + self._spacing
|
new_height = max_bottom + self._spacing
|
||||||
|
|
||||||
# Update size if needed
|
# Update size if needed
|
||||||
if new_height > self._size[1]:
|
if new_height > self._size[1]:
|
||||||
self._size = np.array([self._content_width, new_height])
|
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:
|
def get_content_height(self) -> int:
|
||||||
"""Get the total content height"""
|
"""Get the total content height"""
|
||||||
self._update_content_height()
|
self._update_content_height()
|
||||||
|
|||||||
@ -20,8 +20,8 @@ import pyperclip
|
|||||||
|
|
||||||
# Import pyWebLayout components including the new viewport system
|
# Import pyWebLayout components including the new viewport system
|
||||||
from pyWebLayout.concrete import (
|
from pyWebLayout.concrete import (
|
||||||
Page, Box, Text, RenderableImage,
|
Page, Box, Text, RenderableImage,
|
||||||
RenderableLink, RenderableButton, RenderableForm, RenderableFormField,
|
LinkText, ButtonText, FormFieldText,
|
||||||
Viewport, ScrollablePageContent
|
Viewport, ScrollablePageContent
|
||||||
)
|
)
|
||||||
from pyWebLayout.abstract.functional import (
|
from pyWebLayout.abstract.functional import (
|
||||||
@ -53,17 +53,37 @@ class HTMLViewportAdapter:
|
|||||||
# Create scrollable content container
|
# Create scrollable content container
|
||||||
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
|
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
|
||||||
|
|
||||||
# Convert abstract blocks to renderable objects using Page's conversion system
|
# Create simple text elements from the blocks
|
||||||
page = Page(size=(viewport_size[0], 10000)) # Large temporary page
|
try:
|
||||||
|
from PIL import ImageDraw
|
||||||
|
|
||||||
# Add blocks to page and let it handle the conversion
|
temp_img = Image.new('RGB', (1, 1))
|
||||||
for i, block in enumerate(blocks):
|
draw = ImageDraw.Draw(temp_img)
|
||||||
renderable = page._convert_block_to_renderable(block)
|
|
||||||
if renderable:
|
for i, block in enumerate(blocks):
|
||||||
content.add_child(renderable)
|
# Create simple text representation
|
||||||
# Add spacing between blocks (but not after the last block)
|
if isinstance(block, Paragraph):
|
||||||
if i < len(blocks) - 1:
|
# Extract text from words in the paragraph
|
||||||
content.add_child(Box((0, 0), (1, 8)))
|
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
|
# Create viewport and add the content
|
||||||
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
|
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user