From e6c17ef8a8ba28a2d153994e8c01941d4ed5fbd8 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Tue, 4 Nov 2025 23:34:23 +0100 Subject: [PATCH] updating viewport --- examples/README.md | 19 +++++ .../README_BROWSER_VIEWPORT.md | 0 pyWebLayout/concrete/__init__.py | 1 + pyWebLayout/concrete/viewport.py | 84 ++++++++++++------- .../examples/html_browser_with_viewport.py | 46 +++++++--- 5 files changed, 108 insertions(+), 42 deletions(-) rename VIEWPORT_SYSTEM_README.md => examples/README_BROWSER_VIEWPORT.md (100%) diff --git a/examples/README.md b/examples/README.md index eef6d8e..c12e61a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/VIEWPORT_SYSTEM_README.md b/examples/README_BROWSER_VIEWPORT.md similarity index 100% rename from VIEWPORT_SYSTEM_README.md rename to examples/README_BROWSER_VIEWPORT.md diff --git a/pyWebLayout/concrete/__init__.py b/pyWebLayout/concrete/__init__.py index 4e2f210..675ac54 100644 --- a/pyWebLayout/concrete/__init__.py +++ b/pyWebLayout/concrete/__init__.py @@ -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 diff --git a/pyWebLayout/concrete/viewport.py b/pyWebLayout/concrete/viewport.py index dbff7f3..3761753 100644 --- a/pyWebLayout/concrete/viewport.py +++ b/pyWebLayout/concrete/viewport.py @@ -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) @@ -126,8 +123,6 @@ class Viewport(Box, Layoutable): max(max_x, self._viewport_size[0]), 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""" @@ -135,7 +130,8 @@ class Viewport(Box, Layoutable): 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() diff --git a/pyWebLayout/examples/html_browser_with_viewport.py b/pyWebLayout/examples/html_browser_with_viewport.py index 8603fe2..390c008 100644 --- a/pyWebLayout/examples/html_browser_with_viewport.py +++ b/pyWebLayout/examples/html_browser_with_viewport.py @@ -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 - - # 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))) + # Create simple text elements from the blocks + try: + from PIL import ImageDraw + + 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))