removed scrolling viewport
This commit is contained in:
parent
e6c17ef8a8
commit
5e3170497e
@ -43,24 +43,6 @@ 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
|
||||||
@ -78,7 +60,6 @@ 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
|
||||||
|
|||||||
@ -1,221 +0,0 @@
|
|||||||
# pyWebLayout Viewport System
|
|
||||||
|
|
||||||
The viewport system provides a movable window into large content areas, enabling efficient scrolling and memory usage for documents of any size. This complements your existing pagination system by allowing smooth scrolling within pages.
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
### 1. Viewport Class (`pyWebLayout.concrete.Viewport`)
|
|
||||||
|
|
||||||
A viewport that provides a movable window into a larger content area.
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Only renders visible content (efficient memory usage)
|
|
||||||
- Supports smooth scrolling in all directions
|
|
||||||
- Provides hit testing for element interaction
|
|
||||||
- Caches content bounds for performance
|
|
||||||
- Auto-calculates content size or accepts explicit sizing
|
|
||||||
|
|
||||||
**Basic Usage:**
|
|
||||||
```python
|
|
||||||
from pyWebLayout.concrete import Viewport, ScrollablePageContent
|
|
||||||
|
|
||||||
# Create viewport
|
|
||||||
viewport = Viewport(viewport_size=(800, 600))
|
|
||||||
|
|
||||||
# Add content
|
|
||||||
content = ScrollablePageContent(content_width=800)
|
|
||||||
content.add_child(some_renderable_element)
|
|
||||||
viewport.add_content(content)
|
|
||||||
|
|
||||||
# Scroll and render
|
|
||||||
viewport.scroll_to(0, 100)
|
|
||||||
image = viewport.render()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. ScrollablePageContent Class
|
|
||||||
|
|
||||||
A specialized container designed to work with viewports for page content that can grow dynamically.
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
- Auto-adjusts height as content is added
|
|
||||||
- Optimized for vertical scrolling layouts
|
|
||||||
- Maintains proper content positioning
|
|
||||||
|
|
||||||
### 3. Enhanced HTML Browser
|
|
||||||
|
|
||||||
The new `html_browser_with_viewport.py` demonstrates the viewport system in action:
|
|
||||||
|
|
||||||
**Enhanced Features:**
|
|
||||||
- **Mouse Wheel Scrolling**: Smooth scrolling with configurable speed
|
|
||||||
- **Keyboard Navigation**: Page Up/Down, Home/End, Arrow keys
|
|
||||||
- **Scrollbar Integration**: Traditional scrollbar with viewport synchronization
|
|
||||||
- **Text Selection**: Works across viewport boundaries
|
|
||||||
- **Memory Efficient**: Only renders visible content
|
|
||||||
|
|
||||||
## Integration with Existing System
|
|
||||||
|
|
||||||
The viewport system complements your existing pagination system:
|
|
||||||
|
|
||||||
1. **Pages**: Still handle content layout and organization
|
|
||||||
2. **Viewport**: Provides efficient viewing and scrolling within pages
|
|
||||||
3. **Pagination**: Can be used for chapter/section navigation
|
|
||||||
4. **Viewport Scrolling**: Handles smooth navigation within content
|
|
||||||
|
|
||||||
## Scrolling Methods
|
|
||||||
|
|
||||||
The viewport supports multiple scrolling methods:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Direct positioning
|
|
||||||
viewport.scroll_to(x, y)
|
|
||||||
viewport.scroll_by(dx, dy)
|
|
||||||
|
|
||||||
# Convenience methods
|
|
||||||
viewport.scroll_to_top()
|
|
||||||
viewport.scroll_to_bottom()
|
|
||||||
viewport.scroll_page_up()
|
|
||||||
viewport.scroll_page_down()
|
|
||||||
viewport.scroll_line_up(line_height)
|
|
||||||
viewport.scroll_line_down(line_height)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Benefits
|
|
||||||
|
|
||||||
### Memory Efficiency
|
|
||||||
- Only visible elements are rendered
|
|
||||||
- Large documents don't consume excessive memory
|
|
||||||
- Content bounds are cached for fast intersection testing
|
|
||||||
|
|
||||||
### Rendering Efficiency
|
|
||||||
- Only renders what's visible in the viewport
|
|
||||||
- Supports partial element rendering (clipping)
|
|
||||||
- Fast hit testing for interaction
|
|
||||||
|
|
||||||
### Scalability
|
|
||||||
- Handles documents of any size
|
|
||||||
- Performance doesn't degrade with content size
|
|
||||||
- Efficient scrolling regardless of document length
|
|
||||||
|
|
||||||
## Browser Integration
|
|
||||||
|
|
||||||
The enhanced browser (`html_browser_with_viewport.py`) provides:
|
|
||||||
|
|
||||||
### User Interface
|
|
||||||
- **Scrollbar**: Traditional scrollbar showing position and size
|
|
||||||
- **Scroll Info**: Real-time scroll progress display
|
|
||||||
- **Multiple Input Methods**: Mouse, keyboard, and scrollbar
|
|
||||||
|
|
||||||
### Interaction
|
|
||||||
- **Hit Testing**: Click detection works within viewport
|
|
||||||
- **Text Selection**: Select and copy text across viewport boundaries
|
|
||||||
- **Link Navigation**: Clickable links work normally
|
|
||||||
|
|
||||||
### Navigation
|
|
||||||
- **Smooth Scrolling**: Configurable scroll speed and behavior
|
|
||||||
- **Page Navigation**: Full page scrolling with configurable overlap
|
|
||||||
- **Precision Control**: Line-by-line scrolling for fine positioning
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### Viewport Methods
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Scrolling
|
|
||||||
viewport.scroll_to(x, y) # Absolute positioning
|
|
||||||
viewport.scroll_by(dx, dy) # Relative movement
|
|
||||||
viewport.scroll_to_top() # Jump to top
|
|
||||||
viewport.scroll_to_bottom() # Jump to bottom
|
|
||||||
viewport.scroll_page_up() # Page up
|
|
||||||
viewport.scroll_page_down() # Page down
|
|
||||||
viewport.scroll_line_up(pixels) # Line up
|
|
||||||
viewport.scroll_line_down(pixels) # Line down
|
|
||||||
|
|
||||||
# Information
|
|
||||||
viewport.get_scroll_info() # Detailed scroll state
|
|
||||||
viewport.get_visible_elements() # Currently visible elements
|
|
||||||
viewport.hit_test(point) # Find element at point
|
|
||||||
|
|
||||||
# Content Management
|
|
||||||
viewport.add_content(renderable) # Add content
|
|
||||||
viewport.clear_content() # Remove all content
|
|
||||||
viewport.set_content_size(size) # Set explicit size
|
|
||||||
```
|
|
||||||
|
|
||||||
### Properties
|
|
||||||
|
|
||||||
```python
|
|
||||||
viewport.viewport_size # (width, height) of viewport
|
|
||||||
viewport.content_size # (width, height) of content
|
|
||||||
viewport.viewport_offset # (x, y) scroll position
|
|
||||||
viewport.max_scroll_x # Maximum horizontal scroll
|
|
||||||
viewport.max_scroll_y # Maximum vertical scroll
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use Cases
|
|
||||||
|
|
||||||
### 1. Document Viewing
|
|
||||||
Perfect for viewing long documents, articles, or books where you need to scroll through content smoothly.
|
|
||||||
|
|
||||||
### 2. Web Page Rendering
|
|
||||||
Ideal for HTML rendering where pages can be very long but you only want to render the visible portion.
|
|
||||||
|
|
||||||
### 3. Large Data Visualization
|
|
||||||
Useful for rendering large datasets or complex layouts where only a portion is visible at any time.
|
|
||||||
|
|
||||||
### 4. Mobile-Style Interfaces
|
|
||||||
Enables smooth scrolling interfaces similar to mobile applications.
|
|
||||||
|
|
||||||
## Example: Basic Viewport Usage
|
|
||||||
|
|
||||||
```python
|
|
||||||
from pyWebLayout.concrete import Viewport, ScrollablePageContent, Text
|
|
||||||
from pyWebLayout.style.fonts import Font
|
|
||||||
|
|
||||||
# Create content
|
|
||||||
content = ScrollablePageContent(content_width=800)
|
|
||||||
|
|
||||||
# Add lots of text
|
|
||||||
font = Font(font_size=14)
|
|
||||||
for i in range(100):
|
|
||||||
text = Text(f"This is line {i+1} of content", font)
|
|
||||||
content.add_child(text)
|
|
||||||
|
|
||||||
# Create viewport
|
|
||||||
viewport = Viewport(viewport_size=(800, 600))
|
|
||||||
viewport.add_content(content)
|
|
||||||
|
|
||||||
# Scroll and render
|
|
||||||
viewport.scroll_to(0, 500) # Scroll down 500 pixels
|
|
||||||
image = viewport.render() # Only renders visible content
|
|
||||||
|
|
||||||
# Get scroll information
|
|
||||||
scroll_info = viewport.get_scroll_info()
|
|
||||||
print(f"Scroll progress: {scroll_info['scroll_progress_y']:.1%}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example: Browser Integration
|
|
||||||
|
|
||||||
```python
|
|
||||||
# In your HTML browser
|
|
||||||
def handle_mouse_wheel(self, event):
|
|
||||||
if event.delta > 0:
|
|
||||||
self.viewport.scroll_line_up(20)
|
|
||||||
else:
|
|
||||||
self.viewport.scroll_line_down(20)
|
|
||||||
self.update_display()
|
|
||||||
|
|
||||||
def handle_page_down(self, event):
|
|
||||||
self.viewport.scroll_page_down()
|
|
||||||
self.update_display()
|
|
||||||
|
|
||||||
def update_display(self):
|
|
||||||
image = self.viewport.render()
|
|
||||||
self.display_image(image)
|
|
||||||
self.update_scrollbar()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The viewport system provides a powerful and efficient way to handle large content areas while maintaining smooth user interaction. It integrates seamlessly with your existing pyWebLayout architecture and provides the foundation for building sophisticated document viewers and web browsers.
|
|
||||||
|
|
||||||
The system is designed to be both easy to use for simple cases and powerful enough for complex applications, making it a valuable addition to the pyWebLayout toolkit.
|
|
||||||
@ -3,4 +3,3 @@ 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
|
|
||||||
|
|||||||
@ -1,479 +0,0 @@
|
|||||||
from typing import List, Tuple, Optional, Dict, Any
|
|
||||||
import numpy as np
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from pyWebLayout.core.base import Renderable, Layoutable
|
|
||||||
from .box import Box
|
|
||||||
from pyWebLayout.style import Alignment
|
|
||||||
|
|
||||||
|
|
||||||
class Viewport(Box, Layoutable):
|
|
||||||
"""
|
|
||||||
A viewport that provides a movable window into a larger content area.
|
|
||||||
|
|
||||||
This class allows you to have a large layout containing many elements,
|
|
||||||
but only render the portion that's currently visible in the viewport.
|
|
||||||
This enables efficient scrolling and memory usage for large documents.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, viewport_size: Tuple[int, int], content_size: Optional[Tuple[int, int]] = None,
|
|
||||||
origin=(0, 0), callback=None, sheet=None, mode='RGBA',
|
|
||||||
background_color=(255, 255, 255)):
|
|
||||||
"""
|
|
||||||
Initialize a viewport.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
viewport_size: The size of the visible viewport window (width, height)
|
|
||||||
content_size: The total size of the content area (None for auto-sizing)
|
|
||||||
origin: The origin of the viewport in its parent container
|
|
||||||
callback: Optional callback function
|
|
||||||
sheet: Optional image sheet
|
|
||||||
mode: Image mode
|
|
||||||
background_color: Background color for the viewport
|
|
||||||
"""
|
|
||||||
super().__init__(origin, viewport_size, callback, sheet, mode)
|
|
||||||
|
|
||||||
self._viewport_size = np.array(viewport_size)
|
|
||||||
self._content_size = np.array(content_size) if content_size else None
|
|
||||||
self._background_color = background_color
|
|
||||||
|
|
||||||
# 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
|
|
||||||
self._cache_dirty = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def viewport_size(self) -> Tuple[int, int]:
|
|
||||||
"""Get the viewport size"""
|
|
||||||
return tuple(self._viewport_size)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content_size(self) -> Tuple[int, int]:
|
|
||||||
"""Get the total content size"""
|
|
||||||
if self._content_size is not None:
|
|
||||||
return tuple(self._content_size)
|
|
||||||
else:
|
|
||||||
# Auto-calculate from content
|
|
||||||
self._update_content_size()
|
|
||||||
return tuple(self._content_size)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def viewport_offset(self) -> Tuple[int, int]:
|
|
||||||
"""Get the current viewport offset (scroll position)"""
|
|
||||||
return tuple(self._viewport_offset)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_scroll_x(self) -> int:
|
|
||||||
"""Get the maximum horizontal scroll position"""
|
|
||||||
content_w, content_h = self.content_size
|
|
||||||
viewport_w, viewport_h = self.viewport_size
|
|
||||||
return max(0, content_w - viewport_w)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_scroll_y(self) -> int:
|
|
||||||
"""Get the maximum vertical scroll position"""
|
|
||||||
content_w, content_h = self.content_size
|
|
||||||
viewport_w, viewport_h = self.viewport_size
|
|
||||||
return max(0, content_h - viewport_h)
|
|
||||||
|
|
||||||
def add_content(self, renderable: Renderable) -> 'Viewport':
|
|
||||||
"""Add content to the viewport's content area"""
|
|
||||||
self._content_items.append(renderable)
|
|
||||||
self._cache_dirty = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def clear_content(self) -> 'Viewport':
|
|
||||||
"""Clear all content from the viewport"""
|
|
||||||
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._cache_dirty = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _update_content_size(self):
|
|
||||||
"""Auto-calculate content size from children"""
|
|
||||||
if not self._content_items:
|
|
||||||
self._content_size = self._viewport_size.copy()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find the bounds of all children
|
|
||||||
max_x = 0
|
|
||||||
max_y = 0
|
|
||||||
|
|
||||||
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)
|
|
||||||
child_end = child_origin + child_size
|
|
||||||
|
|
||||||
max_x = max(max_x, child_end[0])
|
|
||||||
max_y = max(max_y, child_end[1])
|
|
||||||
|
|
||||||
# Ensure content size is at least as large as viewport
|
|
||||||
self._content_size = np.array([
|
|
||||||
max(max_x, self._viewport_size[0]),
|
|
||||||
max(max_y, self._viewport_size[1])
|
|
||||||
])
|
|
||||||
|
|
||||||
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 = []
|
|
||||||
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
|
|
||||||
return bounds
|
|
||||||
|
|
||||||
def _collect_element_bounds(self, container, offset: np.ndarray, bounds: List[Tuple]):
|
|
||||||
"""Recursively collect bounds of all renderable elements"""
|
|
||||||
if not hasattr(container, '_children'):
|
|
||||||
return
|
|
||||||
|
|
||||||
for child in container._children:
|
|
||||||
if hasattr(child, '_origin') and hasattr(child, '_size'):
|
|
||||||
child_origin = np.array(child._origin)
|
|
||||||
child_size = np.array(child._size)
|
|
||||||
|
|
||||||
# Calculate absolute position
|
|
||||||
abs_origin = offset + child_origin
|
|
||||||
abs_end = abs_origin + child_size
|
|
||||||
|
|
||||||
# Store bounds info
|
|
||||||
bounds.append((child, abs_origin, abs_end, child_size))
|
|
||||||
|
|
||||||
# Recursively process children
|
|
||||||
if hasattr(child, '_children'):
|
|
||||||
self._collect_element_bounds(child, abs_origin, bounds)
|
|
||||||
|
|
||||||
def scroll_to(self, x: int, y: int) -> 'Viewport':
|
|
||||||
"""
|
|
||||||
Scroll the viewport to a specific position.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
x: Horizontal scroll position
|
|
||||||
y: Vertical scroll position
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Self for method chaining
|
|
||||||
"""
|
|
||||||
# Clamp scroll position to valid range
|
|
||||||
max_x = self.max_scroll_x
|
|
||||||
max_y = self.max_scroll_y
|
|
||||||
|
|
||||||
self._viewport_offset[0] = max(0, min(x, max_x))
|
|
||||||
self._viewport_offset[1] = max(0, min(y, max_y))
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def scroll_by(self, dx: int, dy: int) -> 'Viewport':
|
|
||||||
"""
|
|
||||||
Scroll the viewport by a relative amount.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dx: Horizontal scroll delta
|
|
||||||
dy: Vertical scroll delta
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Self for method chaining
|
|
||||||
"""
|
|
||||||
current_x, current_y = self.viewport_offset
|
|
||||||
return self.scroll_to(current_x + dx, current_y + dy)
|
|
||||||
|
|
||||||
def scroll_to_top(self) -> 'Viewport':
|
|
||||||
"""Scroll to the top of the content"""
|
|
||||||
return self.scroll_to(self._viewport_offset[0], 0)
|
|
||||||
|
|
||||||
def scroll_to_bottom(self) -> 'Viewport':
|
|
||||||
"""Scroll to the bottom of the content"""
|
|
||||||
return self.scroll_to(self._viewport_offset[0], self.max_scroll_y)
|
|
||||||
|
|
||||||
def scroll_page_up(self) -> 'Viewport':
|
|
||||||
"""Scroll up by one viewport height"""
|
|
||||||
return self.scroll_by(0, -self._viewport_size[1])
|
|
||||||
|
|
||||||
def scroll_page_down(self) -> 'Viewport':
|
|
||||||
"""Scroll down by one viewport height"""
|
|
||||||
return self.scroll_by(0, self._viewport_size[1])
|
|
||||||
|
|
||||||
def scroll_line_up(self, line_height: int = 20) -> 'Viewport':
|
|
||||||
"""Scroll up by one line"""
|
|
||||||
return self.scroll_by(0, -line_height)
|
|
||||||
|
|
||||||
def scroll_line_down(self, line_height: int = 20) -> 'Viewport':
|
|
||||||
"""Scroll down by one line"""
|
|
||||||
return self.scroll_by(0, line_height)
|
|
||||||
|
|
||||||
def get_visible_elements(self) -> List[Tuple]:
|
|
||||||
"""
|
|
||||||
Get all elements that are currently visible in the viewport.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of tuples (element, visible_origin, visible_size) for each visible element
|
|
||||||
"""
|
|
||||||
# Define viewport bounds
|
|
||||||
viewport_left = self._viewport_offset[0]
|
|
||||||
viewport_top = self._viewport_offset[1]
|
|
||||||
viewport_right = viewport_left + self._viewport_size[0]
|
|
||||||
viewport_bottom = viewport_top + self._viewport_size[1]
|
|
||||||
|
|
||||||
visible_elements = []
|
|
||||||
content_bounds = self._get_content_bounds()
|
|
||||||
|
|
||||||
for element, abs_origin, abs_end, element_size in content_bounds:
|
|
||||||
# Check if element intersects with viewport
|
|
||||||
if (abs_origin[0] < viewport_right and abs_end[0] > viewport_left and
|
|
||||||
abs_origin[1] < viewport_bottom and abs_end[1] > viewport_top):
|
|
||||||
|
|
||||||
# Calculate visible portion of the element
|
|
||||||
visible_left = max(abs_origin[0], viewport_left)
|
|
||||||
visible_top = max(abs_origin[1], viewport_top)
|
|
||||||
visible_right = min(abs_end[0], viewport_right)
|
|
||||||
visible_bottom = min(abs_end[1], viewport_bottom)
|
|
||||||
|
|
||||||
# Calculate visible origin relative to viewport
|
|
||||||
visible_origin = np.array([
|
|
||||||
visible_left - viewport_left,
|
|
||||||
visible_top - viewport_top
|
|
||||||
])
|
|
||||||
|
|
||||||
# Calculate visible size
|
|
||||||
visible_size = np.array([
|
|
||||||
visible_right - visible_left,
|
|
||||||
visible_bottom - visible_top
|
|
||||||
])
|
|
||||||
|
|
||||||
# Calculate clipping info for the element
|
|
||||||
element_clip_x = visible_left - abs_origin[0]
|
|
||||||
element_clip_y = visible_top - abs_origin[1]
|
|
||||||
element_clip_w = visible_size[0]
|
|
||||||
element_clip_h = visible_size[1]
|
|
||||||
|
|
||||||
visible_elements.append((
|
|
||||||
element,
|
|
||||||
visible_origin,
|
|
||||||
visible_size,
|
|
||||||
(element_clip_x, element_clip_y, element_clip_w, element_clip_h)
|
|
||||||
))
|
|
||||||
|
|
||||||
return visible_elements
|
|
||||||
|
|
||||||
def layout(self):
|
|
||||||
"""Layout the content within the viewport"""
|
|
||||||
# Update content size if needed
|
|
||||||
if self._content_size is None:
|
|
||||||
self._update_content_size()
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
"""
|
|
||||||
Render only the visible portion of the content.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A PIL Image containing the rendered viewport
|
|
||||||
"""
|
|
||||||
# Ensure content is laid out
|
|
||||||
self.layout()
|
|
||||||
|
|
||||||
# Create viewport canvas
|
|
||||||
canvas = Image.new(self._mode, tuple(self._viewport_size), self._background_color)
|
|
||||||
|
|
||||||
# Get visible elements
|
|
||||||
visible_elements = self.get_visible_elements()
|
|
||||||
|
|
||||||
# Render each visible element
|
|
||||||
for element, visible_origin, visible_size, clip_info in visible_elements:
|
|
||||||
try:
|
|
||||||
# Render the full element
|
|
||||||
element_img = element.render()
|
|
||||||
|
|
||||||
# Extract the visible portion
|
|
||||||
clip_x, clip_y, clip_w, clip_h = clip_info
|
|
||||||
if clip_x >= 0 and clip_y >= 0 and clip_w > 0 and clip_h > 0:
|
|
||||||
# Ensure clipping bounds are within element image
|
|
||||||
elem_w, elem_h = element_img.size
|
|
||||||
clip_x = min(clip_x, elem_w)
|
|
||||||
clip_y = min(clip_y, elem_h)
|
|
||||||
clip_w = min(clip_w, elem_w - clip_x)
|
|
||||||
clip_h = min(clip_h, elem_h - clip_y)
|
|
||||||
|
|
||||||
if clip_w > 0 and clip_h > 0:
|
|
||||||
# Crop the visible portion
|
|
||||||
visible_img = element_img.crop((
|
|
||||||
clip_x, clip_y,
|
|
||||||
clip_x + clip_w, clip_y + clip_h
|
|
||||||
))
|
|
||||||
|
|
||||||
# Paste onto viewport canvas
|
|
||||||
paste_pos = tuple(visible_origin.astype(int))
|
|
||||||
if visible_img.mode == 'RGBA' and canvas.mode == 'RGBA':
|
|
||||||
canvas.paste(visible_img, paste_pos, visible_img)
|
|
||||||
else:
|
|
||||||
canvas.paste(visible_img, paste_pos)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Skip elements that fail to render
|
|
||||||
continue
|
|
||||||
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
def hit_test(self, point: Tuple[int, int]) -> Optional[Renderable]:
|
|
||||||
"""
|
|
||||||
Find the topmost element at the given viewport coordinates.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
point: Coordinates within the viewport
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The element at the given point, or None
|
|
||||||
"""
|
|
||||||
viewport_x, viewport_y = point
|
|
||||||
|
|
||||||
# Convert viewport coordinates to content coordinates
|
|
||||||
content_x = viewport_x + self._viewport_offset[0]
|
|
||||||
content_y = viewport_y + self._viewport_offset[1]
|
|
||||||
|
|
||||||
# Find elements at this point (in reverse order for top-to-bottom hit testing)
|
|
||||||
content_bounds = self._get_content_bounds()
|
|
||||||
|
|
||||||
for element, abs_origin, abs_end, element_size in reversed(content_bounds):
|
|
||||||
if (abs_origin[0] <= content_x < abs_end[0] and
|
|
||||||
abs_origin[1] <= content_y < abs_end[1]):
|
|
||||||
return element
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_scroll_info(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Get information about the current scroll state.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with scroll information
|
|
||||||
"""
|
|
||||||
content_w, content_h = self.content_size
|
|
||||||
viewport_w, viewport_h = self.viewport_size
|
|
||||||
offset_x, offset_y = self.viewport_offset
|
|
||||||
|
|
||||||
return {
|
|
||||||
'content_size': (content_w, content_h),
|
|
||||||
'viewport_size': (viewport_w, viewport_h),
|
|
||||||
'offset': (offset_x, offset_y),
|
|
||||||
'max_scroll': (self.max_scroll_x, self.max_scroll_y),
|
|
||||||
'scroll_progress_x': offset_x / max(1, self.max_scroll_x) if self.max_scroll_x > 0 else 0,
|
|
||||||
'scroll_progress_y': offset_y / max(1, self.max_scroll_y) if self.max_scroll_y > 0 else 0,
|
|
||||||
'can_scroll_up': offset_y > 0,
|
|
||||||
'can_scroll_down': offset_y < self.max_scroll_y,
|
|
||||||
'can_scroll_left': offset_x > 0,
|
|
||||||
'can_scroll_right': offset_x < self.max_scroll_x
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ScrollablePageContent(Box):
|
|
||||||
"""
|
|
||||||
A specialized container for page content that's designed to work with viewports.
|
|
||||||
This extends the regular Box functionality but allows for much larger content areas.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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)
|
|
||||||
"""
|
|
||||||
super().__init__(
|
|
||||||
origin=(0, 0),
|
|
||||||
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"""
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _update_content_height(self):
|
|
||||||
"""Update the content height based on children"""
|
|
||||||
if not self._children:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find the bottom-most child
|
|
||||||
max_bottom = 0
|
|
||||||
for child in self._children:
|
|
||||||
if hasattr(child, '_origin') and hasattr(child, '_size'):
|
|
||||||
child_bottom = child._origin[1] + child._size[1]
|
|
||||||
max_bottom = max(max_bottom, child_bottom)
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
return self._size[1]
|
|
||||||
@ -1,224 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Demo script showing the viewport system in action.
|
|
||||||
|
|
||||||
This demonstrates how the viewport provides a movable window into large content,
|
|
||||||
enabling efficient scrolling without rendering the entire content at once.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from PIL import Image
|
|
||||||
from pyWebLayout.concrete import (
|
|
||||||
Viewport, ScrollablePageContent, Text, Box, RenderableImage
|
|
||||||
)
|
|
||||||
from pyWebLayout.style.fonts import Font, FontWeight
|
|
||||||
from pyWebLayout.style import Alignment
|
|
||||||
|
|
||||||
|
|
||||||
def create_large_document_content():
|
|
||||||
"""Create a large document to demonstrate viewport scrolling"""
|
|
||||||
|
|
||||||
# Create scrollable content container
|
|
||||||
content = ScrollablePageContent(content_width=800, initial_height=100)
|
|
||||||
|
|
||||||
# Add a title
|
|
||||||
title_font = Font(font_size=24, weight=FontWeight.BOLD)
|
|
||||||
title = Text("Large Document Demo", title_font)
|
|
||||||
content.add_child(title)
|
|
||||||
|
|
||||||
# Add spacing
|
|
||||||
content.add_child(Box((0, 0), (1, 20)))
|
|
||||||
|
|
||||||
# Add many paragraphs to create a long document
|
|
||||||
paragraph_font = Font(font_size=14)
|
|
||||||
|
|
||||||
for i in range(50):
|
|
||||||
# Section header
|
|
||||||
section_font = Font(font_size=18, weight=FontWeight.BOLD)
|
|
||||||
header = Text(f"Section {i+1}", section_font)
|
|
||||||
content.add_child(header)
|
|
||||||
|
|
||||||
# Add some spacing
|
|
||||||
content.add_child(Box((0, 0), (1, 10)))
|
|
||||||
|
|
||||||
# Add paragraph content
|
|
||||||
paragraphs = [
|
|
||||||
f"This is paragraph {i+1}, line 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
|
||||||
f"This is paragraph {i+1}, line 2. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
|
||||||
f"This is paragraph {i+1}, line 3. Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
|
|
||||||
f"This is paragraph {i+1}, line 4. Duis aute irure dolor in reprehenderit in voluptate velit esse.",
|
|
||||||
f"This is paragraph {i+1}, line 5. Excepteur sint occaecat cupidatat non proident, sunt in culpa."
|
|
||||||
]
|
|
||||||
|
|
||||||
for para_text in paragraphs:
|
|
||||||
para = Text(para_text, paragraph_font)
|
|
||||||
content.add_child(para)
|
|
||||||
content.add_child(Box((0, 0), (1, 5))) # Line spacing
|
|
||||||
|
|
||||||
# Add section spacing
|
|
||||||
content.add_child(Box((0, 0), (1, 15)))
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def demo_viewport_rendering():
|
|
||||||
"""Demonstrate viewport rendering at different scroll positions"""
|
|
||||||
|
|
||||||
print("Creating large document content...")
|
|
||||||
content = create_large_document_content()
|
|
||||||
|
|
||||||
print(f"Content size: {content.get_content_height()} pixels tall")
|
|
||||||
|
|
||||||
# Create viewport
|
|
||||||
viewport = Viewport(viewport_size=(800, 600), background_color=(255, 255, 255))
|
|
||||||
|
|
||||||
# Add content to viewport
|
|
||||||
viewport.add_content(content)
|
|
||||||
|
|
||||||
print(f"Viewport content size: {viewport.content_size}")
|
|
||||||
print(f"Max scroll Y: {viewport.max_scroll_y}")
|
|
||||||
|
|
||||||
# Create output directory
|
|
||||||
os.makedirs("output/viewport_demo", exist_ok=True)
|
|
||||||
|
|
||||||
# Render viewport at different scroll positions
|
|
||||||
scroll_positions = [
|
|
||||||
(0, "top"),
|
|
||||||
(viewport.max_scroll_y // 4, "quarter"),
|
|
||||||
(viewport.max_scroll_y // 2, "middle"),
|
|
||||||
(viewport.max_scroll_y * 3 // 4, "three_quarters"),
|
|
||||||
(viewport.max_scroll_y, "bottom")
|
|
||||||
]
|
|
||||||
|
|
||||||
for scroll_y, label in scroll_positions:
|
|
||||||
print(f"Rendering viewport at scroll position {scroll_y} ({label})...")
|
|
||||||
|
|
||||||
# Scroll to position
|
|
||||||
viewport.scroll_to(0, scroll_y)
|
|
||||||
|
|
||||||
# Get scroll info
|
|
||||||
scroll_info = viewport.get_scroll_info()
|
|
||||||
print(f" Scroll progress: {scroll_info['scroll_progress_y']:.2%}")
|
|
||||||
print(f" Visible elements: {len(viewport.get_visible_elements())}")
|
|
||||||
|
|
||||||
# Render viewport
|
|
||||||
viewport_img = viewport.render()
|
|
||||||
|
|
||||||
# Save image
|
|
||||||
output_path = f"output/viewport_demo/viewport_{label}.png"
|
|
||||||
viewport_img.save(output_path)
|
|
||||||
print(f" Saved: {output_path}")
|
|
||||||
|
|
||||||
print("\nViewport demo complete!")
|
|
||||||
return viewport
|
|
||||||
|
|
||||||
|
|
||||||
def demo_hit_testing():
|
|
||||||
"""Demonstrate hit testing in the viewport"""
|
|
||||||
|
|
||||||
print("\nTesting hit detection...")
|
|
||||||
|
|
||||||
# Create simple content for hit testing
|
|
||||||
content = ScrollablePageContent(content_width=800, initial_height=100)
|
|
||||||
|
|
||||||
# Add clickable elements
|
|
||||||
for i in range(10):
|
|
||||||
text = Text(f"Clickable text item {i}", Font(font_size=16))
|
|
||||||
content.add_child(text)
|
|
||||||
content.add_child(Box((0, 0), (1, 20))) # Spacing
|
|
||||||
|
|
||||||
# Create viewport
|
|
||||||
viewport = Viewport(viewport_size=(800, 300))
|
|
||||||
viewport.add_content(content)
|
|
||||||
|
|
||||||
# Test hit detection at different scroll positions
|
|
||||||
test_points = [(100, 50), (200, 100), (300, 150)]
|
|
||||||
|
|
||||||
for scroll_y in [0, 100, 200]:
|
|
||||||
viewport.scroll_to(0, scroll_y)
|
|
||||||
print(f"\nAt scroll position {scroll_y}:")
|
|
||||||
|
|
||||||
for point in test_points:
|
|
||||||
hit_element = viewport.hit_test(point)
|
|
||||||
if hit_element:
|
|
||||||
element_text = getattr(hit_element, '_text', 'Unknown element')
|
|
||||||
print(f" Point {point}: Hit '{element_text}'")
|
|
||||||
else:
|
|
||||||
print(f" Point {point}: No element")
|
|
||||||
|
|
||||||
|
|
||||||
def demo_scroll_methods():
|
|
||||||
"""Demonstrate different scrolling methods"""
|
|
||||||
|
|
||||||
print("\nTesting scroll methods...")
|
|
||||||
|
|
||||||
# Create content
|
|
||||||
content = ScrollablePageContent(content_width=800, initial_height=100)
|
|
||||||
|
|
||||||
for i in range(20):
|
|
||||||
text = Text(f"Line {i+1}: This is some sample text for scrolling demo", Font(font_size=14))
|
|
||||||
content.add_child(text)
|
|
||||||
content.add_child(Box((0, 0), (1, 5)))
|
|
||||||
|
|
||||||
# Create viewport
|
|
||||||
viewport = Viewport(viewport_size=(800, 200))
|
|
||||||
viewport.add_content(content)
|
|
||||||
|
|
||||||
print(f"Content height: {viewport.content_size[1]}")
|
|
||||||
print(f"Viewport height: {viewport.viewport_size[1]}")
|
|
||||||
print(f"Max scroll Y: {viewport.max_scroll_y}")
|
|
||||||
|
|
||||||
# Test different scroll methods
|
|
||||||
print("\nTesting scroll methods:")
|
|
||||||
|
|
||||||
# Scroll by lines
|
|
||||||
print("Scrolling down 5 lines...")
|
|
||||||
for i in range(5):
|
|
||||||
viewport.scroll_line_down(20)
|
|
||||||
print(f" After line {i+1}: offset = {viewport.viewport_offset}")
|
|
||||||
|
|
||||||
# Scroll by pages
|
|
||||||
print("Scrolling down 1 page...")
|
|
||||||
viewport.scroll_page_down()
|
|
||||||
print(f" After page down: offset = {viewport.viewport_offset}")
|
|
||||||
|
|
||||||
# Scroll to bottom
|
|
||||||
print("Scrolling to bottom...")
|
|
||||||
viewport.scroll_to_bottom()
|
|
||||||
print(f" At bottom: offset = {viewport.viewport_offset}")
|
|
||||||
|
|
||||||
# Scroll to top
|
|
||||||
print("Scrolling to top...")
|
|
||||||
viewport.scroll_to_top()
|
|
||||||
print(f" At top: offset = {viewport.viewport_offset}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run all viewport demos"""
|
|
||||||
print("=== Viewport System Demo ===")
|
|
||||||
|
|
||||||
# Demo 1: Basic viewport rendering
|
|
||||||
viewport = demo_viewport_rendering()
|
|
||||||
|
|
||||||
# Demo 2: Hit testing
|
|
||||||
demo_hit_testing()
|
|
||||||
|
|
||||||
# Demo 3: Scroll methods
|
|
||||||
demo_scroll_methods()
|
|
||||||
|
|
||||||
print("\n=== Demo Complete ===")
|
|
||||||
print("Check the output/viewport_demo/ directory for rendered images.")
|
|
||||||
|
|
||||||
# Show final scroll info
|
|
||||||
scroll_info = viewport.get_scroll_info()
|
|
||||||
print(f"\nFinal viewport state:")
|
|
||||||
print(f" Content size: {scroll_info['content_size']}")
|
|
||||||
print(f" Viewport size: {scroll_info['viewport_size']}")
|
|
||||||
print(f" Current offset: {scroll_info['offset']}")
|
|
||||||
print(f" Scroll progress: {scroll_info['scroll_progress_y']:.1%}")
|
|
||||||
print(f" Can scroll up: {scroll_info['can_scroll_up']}")
|
|
||||||
print(f" Can scroll down: {scroll_info['can_scroll_down']}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,790 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Enhanced HTML Browser using pyWebLayout with Viewport System
|
|
||||||
|
|
||||||
This browser uses the new viewport system to enable efficient scrolling
|
|
||||||
within HTML pages, only rendering the visible portion of large documents.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk, messagebox, filedialog, simpledialog
|
|
||||||
from PIL import Image, ImageTk, ImageDraw
|
|
||||||
from typing import Dict, List, Optional, Tuple, Any
|
|
||||||
import webbrowser
|
|
||||||
import os
|
|
||||||
from urllib.parse import urljoin, urlparse
|
|
||||||
import requests
|
|
||||||
from io import BytesIO
|
|
||||||
import pyperclip
|
|
||||||
|
|
||||||
# Import pyWebLayout components including the new viewport system
|
|
||||||
from pyWebLayout.concrete import (
|
|
||||||
Page, Box, Text, RenderableImage,
|
|
||||||
LinkText, ButtonText, FormFieldText,
|
|
||||||
Viewport, ScrollablePageContent
|
|
||||||
)
|
|
||||||
from pyWebLayout.abstract.functional import (
|
|
||||||
Link, Button, Form, FormField, LinkType, FormFieldType
|
|
||||||
)
|
|
||||||
from pyWebLayout.abstract.block import Paragraph
|
|
||||||
from pyWebLayout.abstract.inline import Word
|
|
||||||
from pyWebLayout.style.fonts import Font, FontWeight, FontStyle, TextDecoration
|
|
||||||
from pyWebLayout.style import Alignment
|
|
||||||
from pyWebLayout.io.readers.html_extraction import parse_html_string
|
|
||||||
|
|
||||||
|
|
||||||
class HTMLViewportAdapter:
|
|
||||||
"""Adapter to convert HTML to viewport using the proper HTML extraction system"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def parse_html_string(self, html_content: str, base_url: str = "", viewport_size: Tuple[int, int] = (800, 600)) -> Viewport:
|
|
||||||
"""Parse HTML string and return a Viewport object with scrollable content using the proper parser"""
|
|
||||||
# Use the proper HTML extraction system
|
|
||||||
base_font = Font(font_size=14)
|
|
||||||
blocks = parse_html_string(html_content, base_font)
|
|
||||||
|
|
||||||
# Extract title
|
|
||||||
title_match = re.search(r'<title>(.*?)</title>', html_content, re.IGNORECASE)
|
|
||||||
title = title_match.group(1) if title_match else "Untitled"
|
|
||||||
|
|
||||||
# Create scrollable content container
|
|
||||||
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
|
|
||||||
|
|
||||||
# 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))
|
|
||||||
viewport.add_content(content)
|
|
||||||
viewport.title = title
|
|
||||||
|
|
||||||
return viewport
|
|
||||||
|
|
||||||
def parse_html_file(self, file_path: str, viewport_size: Tuple[int, int] = (800, 600)) -> Viewport:
|
|
||||||
"""Parse HTML file and return a Viewport object"""
|
|
||||||
try:
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
html_content = f.read()
|
|
||||||
base_url = os.path.dirname(os.path.abspath(file_path))
|
|
||||||
return self.parse_html_string(html_content, base_url, viewport_size)
|
|
||||||
except Exception as e:
|
|
||||||
# Create error viewport
|
|
||||||
content = ScrollablePageContent(content_width=viewport_size[0] - 20, initial_height=100)
|
|
||||||
error_text = Text(f"Error loading file: {str(e)}", Font(font_size=16, colour=(255, 0, 0)))
|
|
||||||
content.add_child(error_text)
|
|
||||||
|
|
||||||
viewport = Viewport(viewport_size=viewport_size, background_color=(255, 255, 255))
|
|
||||||
viewport.add_content(content)
|
|
||||||
viewport.title = "Error"
|
|
||||||
return viewport
|
|
||||||
|
|
||||||
|
|
||||||
class ViewportBrowserWindow:
|
|
||||||
"""Enhanced browser window using Tkinter with Viewport support"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.root = tk.Tk()
|
|
||||||
self.root.title("pyWebLayout HTML Browser with Viewport")
|
|
||||||
self.root.geometry("1000x800")
|
|
||||||
|
|
||||||
self.current_viewport = None
|
|
||||||
self.history = []
|
|
||||||
self.history_index = -1
|
|
||||||
|
|
||||||
# Scrolling parameters
|
|
||||||
self.scroll_speed = 20 # pixels per scroll
|
|
||||||
self.page_scroll_ratio = 0.9 # fraction of viewport height for page scroll
|
|
||||||
|
|
||||||
# Text selection variables
|
|
||||||
self.selection_start = None
|
|
||||||
self.selection_end = None
|
|
||||||
self.is_selecting = False
|
|
||||||
self.selected_text = ""
|
|
||||||
self.text_elements = []
|
|
||||||
self.selection_overlay = None
|
|
||||||
|
|
||||||
self.setup_ui()
|
|
||||||
|
|
||||||
def setup_ui(self):
|
|
||||||
"""Setup the user interface"""
|
|
||||||
# Create main frame
|
|
||||||
main_frame = ttk.Frame(self.root)
|
|
||||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
||||||
|
|
||||||
# Navigation frame
|
|
||||||
nav_frame = ttk.Frame(main_frame)
|
|
||||||
nav_frame.pack(fill=tk.X, pady=(0, 5))
|
|
||||||
|
|
||||||
# Navigation buttons
|
|
||||||
self.back_btn = ttk.Button(nav_frame, text="←", command=self.go_back, state=tk.DISABLED)
|
|
||||||
self.back_btn.pack(side=tk.LEFT, padx=(0, 5))
|
|
||||||
|
|
||||||
self.forward_btn = ttk.Button(nav_frame, text="→", command=self.go_forward, state=tk.DISABLED)
|
|
||||||
self.forward_btn.pack(side=tk.LEFT, padx=(0, 5))
|
|
||||||
|
|
||||||
self.refresh_btn = ttk.Button(nav_frame, text="⟳", command=self.refresh)
|
|
||||||
self.refresh_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
||||||
|
|
||||||
# Address bar
|
|
||||||
ttk.Label(nav_frame, text="URL:").pack(side=tk.LEFT)
|
|
||||||
self.url_var = tk.StringVar()
|
|
||||||
self.url_entry = ttk.Entry(nav_frame, textvariable=self.url_var, width=50)
|
|
||||||
self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(5, 5))
|
|
||||||
self.url_entry.bind('<Return>', self.navigate_to_url)
|
|
||||||
|
|
||||||
self.go_btn = ttk.Button(nav_frame, text="Go", command=self.navigate_to_url)
|
|
||||||
self.go_btn.pack(side=tk.LEFT, padx=(0, 10))
|
|
||||||
|
|
||||||
# File operations
|
|
||||||
self.open_btn = ttk.Button(nav_frame, text="Open File", command=self.open_file)
|
|
||||||
self.open_btn.pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
# Content frame with scrollbars and viewport controls
|
|
||||||
content_frame = ttk.Frame(main_frame)
|
|
||||||
content_frame.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Create scrollbar frame
|
|
||||||
scroll_frame = ttk.Frame(content_frame)
|
|
||||||
scroll_frame.pack(side=tk.RIGHT, fill=tk.Y)
|
|
||||||
|
|
||||||
# Vertical scrollbar
|
|
||||||
self.v_scrollbar = ttk.Scrollbar(scroll_frame, orient=tk.VERTICAL, command=self.on_scrollbar)
|
|
||||||
self.v_scrollbar.pack(fill=tk.Y)
|
|
||||||
|
|
||||||
# Canvas for displaying viewport content
|
|
||||||
self.canvas = tk.Canvas(content_frame, bg='white')
|
|
||||||
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# Scroll info frame
|
|
||||||
info_frame = ttk.Frame(main_frame)
|
|
||||||
info_frame.pack(fill=tk.X, pady=(5, 0))
|
|
||||||
|
|
||||||
self.scroll_info_var = tk.StringVar(value="")
|
|
||||||
ttk.Label(info_frame, textvariable=self.scroll_info_var).pack(side=tk.LEFT)
|
|
||||||
|
|
||||||
# Status bar
|
|
||||||
self.status_var = tk.StringVar(value="Ready")
|
|
||||||
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=tk.SUNKEN)
|
|
||||||
status_bar.pack(fill=tk.X, pady=(5, 0))
|
|
||||||
|
|
||||||
# Bind events
|
|
||||||
self.canvas.bind('<Button-1>', self.on_click)
|
|
||||||
self.canvas.bind('<B1-Motion>', self.on_drag)
|
|
||||||
self.canvas.bind('<ButtonRelease-1>', self.on_release)
|
|
||||||
self.canvas.bind('<Motion>', self.on_mouse_move)
|
|
||||||
self.canvas.bind('<MouseWheel>', self.on_mouse_wheel) # Windows/Mac
|
|
||||||
self.canvas.bind('<Button-4>', self.on_mouse_wheel) # Linux scroll up
|
|
||||||
self.canvas.bind('<Button-5>', self.on_mouse_wheel) # Linux scroll down
|
|
||||||
|
|
||||||
# Keyboard shortcuts
|
|
||||||
self.root.bind('<Control-c>', self.copy_selection)
|
|
||||||
self.root.bind('<Control-a>', self.select_all)
|
|
||||||
self.root.bind('<Prior>', self.page_up) # Page Up
|
|
||||||
self.root.bind('<Next>', self.page_down) # Page Down
|
|
||||||
self.root.bind('<Home>', self.scroll_to_top) # Home
|
|
||||||
self.root.bind('<End>', self.scroll_to_bottom) # End
|
|
||||||
self.root.bind('<Up>', lambda e: self.scroll_by_lines(-1))
|
|
||||||
self.root.bind('<Down>', lambda e: self.scroll_by_lines(1))
|
|
||||||
|
|
||||||
# Context menu
|
|
||||||
self.setup_context_menu()
|
|
||||||
|
|
||||||
# Make canvas focusable
|
|
||||||
self.canvas.config(highlightthickness=1)
|
|
||||||
self.canvas.focus_set()
|
|
||||||
|
|
||||||
# Load default page
|
|
||||||
self.load_default_page()
|
|
||||||
|
|
||||||
def setup_context_menu(self):
|
|
||||||
"""Setup the right-click context menu"""
|
|
||||||
self.context_menu = tk.Menu(self.root, tearoff=0)
|
|
||||||
self.context_menu.add_command(label="Copy", command=self.copy_selection)
|
|
||||||
self.context_menu.add_command(label="Select All", command=self.select_all)
|
|
||||||
self.context_menu.add_separator()
|
|
||||||
self.context_menu.add_command(label="Scroll to Top", command=self.scroll_to_top)
|
|
||||||
self.context_menu.add_command(label="Scroll to Bottom", command=self.scroll_to_bottom)
|
|
||||||
|
|
||||||
# Bind right-click to show context menu
|
|
||||||
self.canvas.bind('<Button-3>', self.show_context_menu)
|
|
||||||
|
|
||||||
def show_context_menu(self, event):
|
|
||||||
"""Show context menu at mouse position"""
|
|
||||||
try:
|
|
||||||
self.context_menu.tk_popup(event.x_root, event.y_root)
|
|
||||||
finally:
|
|
||||||
self.context_menu.grab_release()
|
|
||||||
|
|
||||||
def on_scrollbar(self, *args):
|
|
||||||
"""Handle scrollbar movement"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
action = args[0]
|
|
||||||
|
|
||||||
if action == "moveto":
|
|
||||||
# Absolute position (0.0 to 1.0)
|
|
||||||
fraction = float(args[1])
|
|
||||||
max_scroll = self.current_viewport.max_scroll_y
|
|
||||||
new_y = int(fraction * max_scroll)
|
|
||||||
self.current_viewport.scroll_to(0, new_y)
|
|
||||||
self.update_viewport_display()
|
|
||||||
|
|
||||||
elif action in ["scroll", "step"]:
|
|
||||||
# Relative movement
|
|
||||||
direction = int(args[1])
|
|
||||||
# Handle the units parameter properly - it might be a string "units"
|
|
||||||
if len(args) > 2:
|
|
||||||
try:
|
|
||||||
units = int(args[2])
|
|
||||||
except ValueError:
|
|
||||||
# If args[2] is "units" string, default to 1
|
|
||||||
units = 1
|
|
||||||
else:
|
|
||||||
units = 1
|
|
||||||
|
|
||||||
if action == "scroll":
|
|
||||||
# Line-based scrolling
|
|
||||||
self.scroll_by_lines(direction * units)
|
|
||||||
elif action == "step":
|
|
||||||
# Page-based scrolling
|
|
||||||
self.scroll_by_pages(direction * units)
|
|
||||||
|
|
||||||
def update_scrollbar(self):
|
|
||||||
"""Update scrollbar position and size based on viewport state"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
self.v_scrollbar.set(0, 1)
|
|
||||||
return
|
|
||||||
|
|
||||||
scroll_info = self.current_viewport.get_scroll_info()
|
|
||||||
content_height = scroll_info['content_size'][1]
|
|
||||||
viewport_height = scroll_info['viewport_size'][1]
|
|
||||||
current_offset = scroll_info['offset'][1]
|
|
||||||
|
|
||||||
if content_height <= viewport_height:
|
|
||||||
# No scrolling needed
|
|
||||||
self.v_scrollbar.set(0, 1)
|
|
||||||
else:
|
|
||||||
# Calculate scrollbar position and size
|
|
||||||
top_fraction = current_offset / content_height
|
|
||||||
bottom_fraction = (current_offset + viewport_height) / content_height
|
|
||||||
self.v_scrollbar.set(top_fraction, bottom_fraction)
|
|
||||||
|
|
||||||
# Update scroll info display
|
|
||||||
progress = scroll_info['scroll_progress_y']
|
|
||||||
self.scroll_info_var.set(f"Scroll: {progress:.1%} ({current_offset}/{content_height - viewport_height})")
|
|
||||||
|
|
||||||
def on_mouse_wheel(self, event):
|
|
||||||
"""Handle mouse wheel scrolling"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Determine scroll direction and amount
|
|
||||||
if event.num == 4 or event.delta > 0:
|
|
||||||
# Scroll up
|
|
||||||
self.scroll_by_lines(-3)
|
|
||||||
elif event.num == 5 or event.delta < 0:
|
|
||||||
# Scroll down
|
|
||||||
self.scroll_by_lines(3)
|
|
||||||
|
|
||||||
def scroll_by_lines(self, lines: int):
|
|
||||||
"""Scroll by a number of lines"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
delta_y = lines * self.scroll_speed
|
|
||||||
self.current_viewport.scroll_by(0, delta_y)
|
|
||||||
self.update_viewport_display()
|
|
||||||
|
|
||||||
def scroll_by_pages(self, pages: int):
|
|
||||||
"""Scroll by a number of pages"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
viewport_height = self.current_viewport.viewport_size[1]
|
|
||||||
delta_y = int(pages * viewport_height * self.page_scroll_ratio)
|
|
||||||
self.current_viewport.scroll_by(0, delta_y)
|
|
||||||
self.update_viewport_display()
|
|
||||||
|
|
||||||
def page_up(self, event=None):
|
|
||||||
"""Scroll up by one page"""
|
|
||||||
self.scroll_by_pages(-1)
|
|
||||||
|
|
||||||
def page_down(self, event=None):
|
|
||||||
"""Scroll down by one page"""
|
|
||||||
self.scroll_by_pages(1)
|
|
||||||
|
|
||||||
def scroll_to_top(self, event=None):
|
|
||||||
"""Scroll to the top of the document"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current_viewport.scroll_to_top()
|
|
||||||
self.update_viewport_display()
|
|
||||||
|
|
||||||
def scroll_to_bottom(self, event=None):
|
|
||||||
"""Scroll to the bottom of the document"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.current_viewport.scroll_to_bottom()
|
|
||||||
self.update_viewport_display()
|
|
||||||
|
|
||||||
def update_viewport_display(self):
|
|
||||||
"""Update the canvas display with current viewport content"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Render the current viewport
|
|
||||||
viewport_img = self.current_viewport.render()
|
|
||||||
|
|
||||||
# Convert to PhotoImage for Tkinter
|
|
||||||
self.photo = ImageTk.PhotoImage(viewport_img)
|
|
||||||
|
|
||||||
# Clear canvas and display image
|
|
||||||
self.canvas.delete("all")
|
|
||||||
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo)
|
|
||||||
|
|
||||||
# Update scrollbar
|
|
||||||
self.update_scrollbar()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set(f"Error rendering viewport: {str(e)}")
|
|
||||||
|
|
||||||
def on_drag(self, event):
|
|
||||||
"""Handle mouse dragging for text selection"""
|
|
||||||
canvas_x = self.canvas.canvasx(event.x)
|
|
||||||
canvas_y = self.canvas.canvasy(event.y)
|
|
||||||
|
|
||||||
if not self.is_selecting:
|
|
||||||
# Start selection
|
|
||||||
self.is_selecting = True
|
|
||||||
self.selection_start = (canvas_x, canvas_y)
|
|
||||||
self.selection_end = (canvas_x, canvas_y)
|
|
||||||
else:
|
|
||||||
# Update selection end
|
|
||||||
self.selection_end = (canvas_x, canvas_y)
|
|
||||||
|
|
||||||
# Update visual selection
|
|
||||||
self.update_selection_visual()
|
|
||||||
|
|
||||||
# Update status
|
|
||||||
self.status_var.set("Selecting text...")
|
|
||||||
|
|
||||||
def on_release(self, event):
|
|
||||||
"""Handle mouse release to complete text selection"""
|
|
||||||
if self.is_selecting:
|
|
||||||
canvas_x = self.canvas.canvasx(event.x)
|
|
||||||
canvas_y = self.canvas.canvasy(event.y)
|
|
||||||
self.selection_end = (canvas_x, canvas_y)
|
|
||||||
|
|
||||||
# Extract selected text
|
|
||||||
self.extract_selected_text()
|
|
||||||
|
|
||||||
# Update status
|
|
||||||
if self.selected_text:
|
|
||||||
self.status_var.set(f"Selected: {len(self.selected_text)} characters")
|
|
||||||
else:
|
|
||||||
self.status_var.set("No text selected")
|
|
||||||
self.clear_selection()
|
|
||||||
|
|
||||||
def update_selection_visual(self):
|
|
||||||
"""Update the visual representation of text selection"""
|
|
||||||
# Remove existing selection overlay
|
|
||||||
if self.selection_overlay:
|
|
||||||
self.canvas.delete(self.selection_overlay)
|
|
||||||
|
|
||||||
if self.selection_start and self.selection_end:
|
|
||||||
# Create selection rectangle
|
|
||||||
x1, y1 = self.selection_start
|
|
||||||
x2, y2 = self.selection_end
|
|
||||||
|
|
||||||
# Ensure proper coordinates (top-left to bottom-right)
|
|
||||||
left = min(x1, x2)
|
|
||||||
top = min(y1, y2)
|
|
||||||
right = max(x1, x2)
|
|
||||||
bottom = max(y1, y2)
|
|
||||||
|
|
||||||
# Draw selection rectangle with transparency effect
|
|
||||||
self.selection_overlay = self.canvas.create_rectangle(
|
|
||||||
left, top, right, bottom,
|
|
||||||
fill='blue', stipple='gray50', outline='blue', width=1
|
|
||||||
)
|
|
||||||
|
|
||||||
def extract_selected_text(self):
|
|
||||||
"""Extract text that falls within the selection area"""
|
|
||||||
if not self.selection_start or not self.selection_end or not self.current_viewport:
|
|
||||||
self.selected_text = ""
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get selection bounds in viewport coordinates
|
|
||||||
x1, y1 = self.selection_start
|
|
||||||
x2, y2 = self.selection_end
|
|
||||||
left = min(x1, x2)
|
|
||||||
top = min(y1, y2)
|
|
||||||
right = max(x1, x2)
|
|
||||||
bottom = max(y1, y2)
|
|
||||||
|
|
||||||
# Convert to content coordinates
|
|
||||||
viewport_offset = self.current_viewport.viewport_offset
|
|
||||||
content_left = left + viewport_offset[0]
|
|
||||||
content_top = top + viewport_offset[1]
|
|
||||||
content_right = right + viewport_offset[0]
|
|
||||||
content_bottom = bottom + viewport_offset[1]
|
|
||||||
|
|
||||||
# Extract text elements in selection area
|
|
||||||
selected_elements = []
|
|
||||||
visible_elements = self.current_viewport.get_visible_elements()
|
|
||||||
|
|
||||||
for element, visible_origin, visible_size, clip_info in visible_elements:
|
|
||||||
# Check if element intersects with selection
|
|
||||||
elem_left = visible_origin[0]
|
|
||||||
elem_top = visible_origin[1]
|
|
||||||
elem_right = elem_left + visible_size[0]
|
|
||||||
elem_bottom = elem_top + visible_size[1]
|
|
||||||
|
|
||||||
if (elem_left < right and elem_right > left and
|
|
||||||
elem_top < bottom and elem_bottom > top):
|
|
||||||
|
|
||||||
# Extract text from element
|
|
||||||
if hasattr(element, '_text'):
|
|
||||||
text_content = element._text
|
|
||||||
if text_content.strip():
|
|
||||||
selected_elements.append((text_content.strip(), elem_left, elem_top))
|
|
||||||
|
|
||||||
# Sort by position (top to bottom, left to right)
|
|
||||||
selected_elements.sort(key=lambda x: (x[2], x[1]))
|
|
||||||
|
|
||||||
# Combine text
|
|
||||||
self.selected_text = " ".join([element[0] for element in selected_elements])
|
|
||||||
|
|
||||||
def copy_selection(self, event=None):
|
|
||||||
"""Copy selected text to clipboard"""
|
|
||||||
if self.selected_text:
|
|
||||||
try:
|
|
||||||
pyperclip.copy(self.selected_text)
|
|
||||||
self.status_var.set(f"Copied {len(self.selected_text)} characters to clipboard")
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set(f"Error copying to clipboard: {str(e)}")
|
|
||||||
else:
|
|
||||||
self.status_var.set("No text selected to copy")
|
|
||||||
|
|
||||||
def select_all(self, event=None):
|
|
||||||
"""Select all text on the page"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Set selection to entire viewport area
|
|
||||||
viewport_size = self.current_viewport.viewport_size
|
|
||||||
|
|
||||||
self.selection_start = (0, 0)
|
|
||||||
self.selection_end = viewport_size
|
|
||||||
self.is_selecting = True
|
|
||||||
|
|
||||||
# Extract all visible text
|
|
||||||
self.extract_selected_text()
|
|
||||||
|
|
||||||
# Update visual
|
|
||||||
self.update_selection_visual()
|
|
||||||
|
|
||||||
if self.selected_text:
|
|
||||||
self.status_var.set(f"Selected all visible text: {len(self.selected_text)} characters")
|
|
||||||
else:
|
|
||||||
self.status_var.set("No text found to select")
|
|
||||||
|
|
||||||
def clear_selection(self):
|
|
||||||
"""Clear the current text selection"""
|
|
||||||
self.selection_start = None
|
|
||||||
self.selection_end = None
|
|
||||||
self.is_selecting = False
|
|
||||||
self.selected_text = ""
|
|
||||||
|
|
||||||
# Remove visual selection
|
|
||||||
if self.selection_overlay:
|
|
||||||
self.canvas.delete(self.selection_overlay)
|
|
||||||
self.selection_overlay = None
|
|
||||||
|
|
||||||
self.status_var.set("Selection cleared")
|
|
||||||
|
|
||||||
def load_default_page(self):
|
|
||||||
"""Load a default welcome page"""
|
|
||||||
html_content = """
|
|
||||||
<html>
|
|
||||||
<head><title>pyWebLayout Browser with Viewport - Welcome</title></head>
|
|
||||||
<body>
|
|
||||||
<h1>Welcome to pyWebLayout Browser with Viewport System</h1>
|
|
||||||
<p>This enhanced browser uses the new viewport system for efficient scrolling through large documents.</p>
|
|
||||||
|
|
||||||
<h2>New Viewport Features:</h2>
|
|
||||||
<ul>
|
|
||||||
<li><b>Efficient Rendering:</b> Only visible content is rendered</li>
|
|
||||||
<li><b>Smooth Scrolling:</b> Mouse wheel, keyboard, and scrollbar support</li>
|
|
||||||
<li><b>Large Document Support:</b> Handle documents of any size</li>
|
|
||||||
<li><b>Memory Efficient:</b> Low memory usage even for huge pages</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Scrolling Controls:</h2>
|
|
||||||
<p><b>Mouse Wheel:</b> Scroll up and down</p>
|
|
||||||
<p><b>Page Up/Down:</b> Scroll by viewport height</p>
|
|
||||||
<p><b>Home/End:</b> Jump to top/bottom</p>
|
|
||||||
<p><b>Arrow Keys:</b> Scroll line by line</p>
|
|
||||||
<p><b>Scrollbar:</b> Click and drag for precise positioning</p>
|
|
||||||
|
|
||||||
<h2>Text Selection:</h2>
|
|
||||||
<p>Click and drag to select text, then use <b>Ctrl+C</b> to copy</p>
|
|
||||||
<p>Use <b>Ctrl+A</b> to select all visible text</p>
|
|
||||||
|
|
||||||
<h3>Try scrolling with different methods!</h3>
|
|
||||||
<p>This page demonstrates the viewport system. All the content above and below is efficiently managed.</p>
|
|
||||||
|
|
||||||
<h2>Sample Content for Scrolling</h2>
|
|
||||||
<p>Here's some additional content to demonstrate scrolling capabilities:</p>
|
|
||||||
|
|
||||||
<h3>Lorem Ipsum</h3>
|
|
||||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
||||||
<p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
|
|
||||||
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
|
|
||||||
|
|
||||||
<h3>More Sample Text</h3>
|
|
||||||
<p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
|
|
||||||
<p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.</p>
|
|
||||||
<p>Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>
|
|
||||||
|
|
||||||
<h3>Even More Content</h3>
|
|
||||||
<p>Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos.</p>
|
|
||||||
<p>Qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.</p>
|
|
||||||
<p>Consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p>
|
|
||||||
|
|
||||||
<h2>Technical Details</h2>
|
|
||||||
<p>The viewport system works by:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Creating a large content container that can hold any amount of content</li>
|
|
||||||
<li>Providing a viewport window that shows only a portion of the content</li>
|
|
||||||
<li>Efficiently calculating which elements are visible</li>
|
|
||||||
<li>Rendering only the visible elements</li>
|
|
||||||
<li>Supporting smooth scrolling through the content</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>This allows handling of very large documents without performance issues.</p>
|
|
||||||
|
|
||||||
<h2>Load Your Own Content</h2>
|
|
||||||
<p>Use the "Open File" button to load local HTML files, or enter a URL in the address bar.</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get current canvas size for viewport
|
|
||||||
self.root.update_idletasks()
|
|
||||||
canvas_width = max(800, self.canvas.winfo_width())
|
|
||||||
canvas_height = max(600, self.canvas.winfo_height())
|
|
||||||
|
|
||||||
parser = HTMLViewportAdapter()
|
|
||||||
self.current_viewport = parser.parse_html_string(html_content, viewport_size=(canvas_width, canvas_height))
|
|
||||||
|
|
||||||
# Update window title
|
|
||||||
if hasattr(self.current_viewport, 'title'):
|
|
||||||
self.root.title(f"pyWebLayout Browser - {self.current_viewport.title}")
|
|
||||||
|
|
||||||
self.update_viewport_display()
|
|
||||||
self.status_var.set("Welcome page loaded with viewport system")
|
|
||||||
|
|
||||||
def navigate_to_url(self, event=None):
|
|
||||||
"""Navigate to the URL in the address bar"""
|
|
||||||
url = self.url_var.get().strip()
|
|
||||||
if not url:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.status_var.set(f"Loading {url}...")
|
|
||||||
self.root.update()
|
|
||||||
|
|
||||||
# Get current canvas size for viewport
|
|
||||||
self.root.update_idletasks()
|
|
||||||
canvas_width = max(800, self.canvas.winfo_width())
|
|
||||||
canvas_height = max(600, self.canvas.winfo_height())
|
|
||||||
|
|
||||||
try:
|
|
||||||
parser = HTMLViewportAdapter()
|
|
||||||
|
|
||||||
if url.startswith(('http://', 'https://')):
|
|
||||||
# Web URL
|
|
||||||
response = requests.get(url, timeout=10)
|
|
||||||
response.raise_for_status()
|
|
||||||
html_content = response.text
|
|
||||||
|
|
||||||
self.current_viewport = parser.parse_html_string(html_content, url, (canvas_width, canvas_height))
|
|
||||||
|
|
||||||
elif os.path.isfile(url):
|
|
||||||
# Local file
|
|
||||||
self.current_viewport = parser.parse_html_file(url, (canvas_width, canvas_height))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Try to treat as a local file path
|
|
||||||
if not url.startswith('file://'):
|
|
||||||
url = 'file://' + os.path.abspath(url)
|
|
||||||
|
|
||||||
file_path = url.replace('file://', '')
|
|
||||||
if os.path.isfile(file_path):
|
|
||||||
self.current_viewport = parser.parse_html_file(file_path, (canvas_width, canvas_height))
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(f"File not found: {file_path}")
|
|
||||||
|
|
||||||
# Update window title
|
|
||||||
if hasattr(self.current_viewport, 'title'):
|
|
||||||
self.root.title(f"pyWebLayout Browser - {self.current_viewport.title}")
|
|
||||||
|
|
||||||
# Add to history
|
|
||||||
self.add_to_history(url)
|
|
||||||
self.update_viewport_display()
|
|
||||||
self.status_var.set(f"Loaded {url}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set(f"Error loading {url}: {str(e)}")
|
|
||||||
messagebox.showerror("Error", f"Failed to load {url}:\n{str(e)}")
|
|
||||||
|
|
||||||
def open_file(self):
|
|
||||||
"""Open a local HTML file"""
|
|
||||||
file_path = filedialog.askopenfilename(
|
|
||||||
title="Open HTML File",
|
|
||||||
filetypes=[("HTML files", "*.html *.htm"), ("All files", "*.*")]
|
|
||||||
)
|
|
||||||
|
|
||||||
if file_path:
|
|
||||||
self.url_var.set(file_path)
|
|
||||||
self.navigate_to_url()
|
|
||||||
|
|
||||||
def on_click(self, event):
|
|
||||||
"""Handle mouse clicks on the canvas"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Convert canvas coordinates to viewport coordinates
|
|
||||||
canvas_x = self.canvas.canvasx(event.x)
|
|
||||||
canvas_y = self.canvas.canvasy(event.y)
|
|
||||||
|
|
||||||
# Use viewport hit testing
|
|
||||||
hit_element = self.current_viewport.hit_test((canvas_x, canvas_y))
|
|
||||||
|
|
||||||
if hit_element and hasattr(hit_element, '_callback'):
|
|
||||||
# Handle clickable elements
|
|
||||||
try:
|
|
||||||
result = hit_element._callback()
|
|
||||||
if result:
|
|
||||||
self.status_var.set(result)
|
|
||||||
|
|
||||||
# For external links, open in system browser
|
|
||||||
if hasattr(hit_element, '_link') and hit_element._link.link_type == LinkType.EXTERNAL:
|
|
||||||
webbrowser.open(hit_element._link.location)
|
|
||||||
except Exception as e:
|
|
||||||
self.status_var.set(f"Click error: {str(e)}")
|
|
||||||
|
|
||||||
def on_mouse_move(self, event):
|
|
||||||
"""Handle mouse movement for hover effects"""
|
|
||||||
if not self.current_viewport:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Convert canvas coordinates to viewport coordinates
|
|
||||||
canvas_x = self.canvas.canvasx(event.x)
|
|
||||||
canvas_y = self.canvas.canvasy(event.y)
|
|
||||||
|
|
||||||
# Check if mouse is over any clickable element
|
|
||||||
hit_element = self.current_viewport.hit_test((canvas_x, canvas_y))
|
|
||||||
|
|
||||||
if hit_element and hasattr(hit_element, '_callback'):
|
|
||||||
self.canvas.configure(cursor="hand2")
|
|
||||||
else:
|
|
||||||
self.canvas.configure(cursor="arrow")
|
|
||||||
|
|
||||||
def add_to_history(self, url):
|
|
||||||
"""Add URL to navigation history"""
|
|
||||||
# Remove any forward history
|
|
||||||
self.history = self.history[:self.history_index + 1]
|
|
||||||
|
|
||||||
# Add new URL
|
|
||||||
self.history.append(url)
|
|
||||||
self.history_index = len(self.history) - 1
|
|
||||||
|
|
||||||
# Update navigation buttons
|
|
||||||
self.update_nav_buttons()
|
|
||||||
|
|
||||||
def update_nav_buttons(self):
|
|
||||||
"""Update the state of navigation buttons"""
|
|
||||||
self.back_btn.configure(state=tk.NORMAL if self.history_index > 0 else tk.DISABLED)
|
|
||||||
self.forward_btn.configure(state=tk.NORMAL if self.history_index < len(self.history) - 1 else tk.DISABLED)
|
|
||||||
|
|
||||||
def go_back(self):
|
|
||||||
"""Navigate back in history"""
|
|
||||||
if self.history_index > 0:
|
|
||||||
self.history_index -= 1
|
|
||||||
url = self.history[self.history_index]
|
|
||||||
self.url_var.set(url)
|
|
||||||
self.navigate_to_url()
|
|
||||||
|
|
||||||
def go_forward(self):
|
|
||||||
"""Navigate forward in history"""
|
|
||||||
if self.history_index < len(self.history) - 1:
|
|
||||||
self.history_index += 1
|
|
||||||
url = self.history[self.history_index]
|
|
||||||
self.url_var.set(url)
|
|
||||||
self.navigate_to_url()
|
|
||||||
|
|
||||||
def refresh(self):
|
|
||||||
"""Refresh the current page"""
|
|
||||||
current_url = self.url_var.get()
|
|
||||||
if current_url:
|
|
||||||
self.navigate_to_url()
|
|
||||||
else:
|
|
||||||
self.load_default_page()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Start the browser"""
|
|
||||||
self.root.mainloop()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main function to run the enhanced browser"""
|
|
||||||
print("Starting pyWebLayout HTML Browser with Viewport System...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
browser = ViewportBrowserWindow()
|
|
||||||
browser.run()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error starting browser: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Loading…
x
Reference in New Issue
Block a user