merge changes from main branch

This commit is contained in:
iap
2025-10-16 11:57:26 -04:00
19 changed files with 428 additions and 298 deletions

View File

@@ -1,239 +0,0 @@
# Rio Framework - Developer Guide for Contributors
## Overview
Rio is a Python-first web framework with a hybrid architecture: Python backend for component logic/state management + TypeScript frontend for DOM rendering. This guide is for contributors developing the Rio framework itself.
## Architecture for Framework Contributors
### Dual Implementation Pattern
- **Python side**: Component definitions, logic, server communication (`/rio/components/`)
- **TypeScript side**: DOM rendering, event handling, client interactions (`/frontend/code/components/`)
- **Communication**: RPC calls and data models via WebSocket/HTTP
- **Build process**: TypeScript compiles to `/rio/frontend files/`, included in Python wheel
### Component Implementation Hierarchy
```
Component (Abstract Base, ComponentMeta)
├── FundamentalComponent (Maps to TypeScript/HTML)
│ ├── Button → _ButtonInternal (TypeScript)
│ ├── ListView → ListView (TypeScript)
│ └── Other built-in components
└── User Components (High-level, composed via build())
```
### Key Internal Systems
- **ComponentMeta**: Metaclass transforming classes into observable dataclasses
- **Observable Properties**: `ComponentProperty` with dependency tracking
- **State Synchronization**: Python ↔ TypeScript via `_custom_serialize_()` and `_on_message_()`
- **Reconciliation**: Component tree diffing and reuse via `_weak_builder_` and `key_to_component`
## Development Environment Setup
### Initial Setup (Required Order)
```bash
# 1. Install Python dependencies
uv sync --all-extras
# 2. Install frontend dependencies
npm install
# 3. Build frontend (REQUIRED before first use)
uv run scripts/build.py
# 4. Setup pre-commit hooks
python -m pre_commit install
```
### Frontend Build System
```bash
# Development build (default)
uv run scripts/build.py
# Production build (for releases)
uv run scripts/build.py --release
```
**When to rebuild frontend:**
- After any TypeScript/SCSS changes in `/frontend/`
- Before testing (some tests require built assets)
- Before releases (production build required)
### Project Structure
- `/rio/components/` - Python component implementations
- `/frontend/code/components/` - TypeScript component implementations
- `/frontend/css/components/` - Component-specific SCSS files
- `/rio/frontend files/` - Compiled frontend assets (generated)
- `/scripts/` - Build and development tools
## Component Development Patterns
### Creating New FundamentalComponents
```python
# Python side (rio/components/my_component.py)
class MyComponent(FundamentalComponent):
text: str = ""
def build_javascript_source(self) -> str:
return JAVASCRIPT_SOURCE_TEMPLATE % {
'js_wrapper_class_name': 'MyComponentWrapper',
'js_user_class_name': 'MyComponent',
'cls_unique_id': self._unique_id_,
}
def _custom_serialize_(self) -> JsonDoc:
return {'text': self.text}
async def _on_message_(self, message: JsonDoc) -> None:
# Handle frontend messages
pass
```
```typescript
// TypeScript side (frontend/code/components/myComponent.ts)
class MyComponent extends ComponentBase {
createElement(): HTMLElement {
return document.createElement('div');
}
updateElement(deltaState: any): void {
if (deltaState.text !== undefined) {
this.element.textContent = deltaState.text;
}
}
}
```
### Component Lifecycle Management
```python
# Component instantiation (ComponentMeta.__call__)
1. Session injection: component._session_ = global_state.currently_building_session
2. ID assignment: component._id_ = session._next_free_component_id
3. Registration: session._newly_created_components.add(component)
4. Property tracking: component._properties_set_by_creator_
5. Key registration: global_state.key_to_component[key] = component
```
### Observable Property System
```python
# Properties automatically track access and changes
@dataclasses.dataclass
class MyComponent(Component):
count: int = 0 # Becomes ComponentProperty
def build(self) -> Component:
# Reading self.count registers dependency
# Changing self.count triggers rebuild
return Text(f"Count: {self.count}")
```
## Testing Framework Contributors
### Test Organization
```bash
# Python backend tests
uv run pytest tests/
# Frontend integration tests (requires browser)
uv run pytest tests/test_frontend/
# Coverage reporting
uv run scripts/code_coverage.py
```
### Test Categories
- **Component tests**: `tests/test_components.py` - Component behavior
- **Observable tests**: `tests/test_observables.py` - State management
- **Frontend tests**: `tests/test_frontend/` - Browser-based testing
- **CLI tests**: `tests/test_cli/` - Command-line interface
## Icon System Development
```bash
# Build icon sets from raw SVGs
uv run scripts/build_icon_set.py
# Icon locations:
# - /raw_icons/ - Source SVG files
# - /thirdparty/bootstrap/icons/ - Bootstrap icons
# - Output: .tar.xz archives → /rio/assets/icon_sets/
```
## Code Quality and Conventions
### Code Standards
```bash
# Python linting and formatting
python -m ruff check .
python -m ruff format .
# TypeScript formatting
npx prettier --write frontend/
# Pre-commit hooks (automatic)
pre-commit run --all-files
```
### Import Patterns
```python
from __future__ import annotations # Always first
import dataclasses
import typing as t
import rio
from .. import global_state, inspection # Relative imports
```
### Type Annotations
- Heavy use of `typing` module with `t.` aliases
- All public APIs fully type-annotated
- Use `@t.final` for non-subclassable classes
- `typing_extensions` for newer features
## Common Development Tasks
### Adding New Built-in Components
1. Create Python component in `/rio/components/`
2. Create TypeScript component in `/frontend/code/components/`
3. Add SCSS styles in `/frontend/css/components/`
4. Update `/rio/components/__init__.py` exports
5. Add tests in `/tests/test_components.py`
6. Build frontend: `uv run scripts/build.py`
### Debugging Component Issues
- Use `rio.global_state` for session inspection
- Check `session._changed_attributes` for state changes
- Frontend: Browser DevTools + component tree visualization
- Backend: Python debugger with component lifecycle hooks
## Integration Points for Contributors
### Python-TypeScript Bridge
- State serialization: `_custom_serialize_()` → TypeScript
- Event handling: TypeScript → `_on_message_()` → Python
- DOM updates: Delta state application in TypeScript
### Session Management
- Session state in `rio/session.py`
- Component registration and lifecycle
- WebSocket communication via `rio/transports/`
## Common Pitfalls for Contributors
### Frontend Development
- **Always rebuild after TypeScript changes**: `uv run scripts/build.py`
- **Component registration**: Each FundamentalComponent needs unique `_unique_id_`
- **State synchronization**: Ensure `_custom_serialize_()` matches TypeScript expectations
- **CSS organization**: Component styles go in `/frontend/css/components/`
### Python Development
- **Observable properties**: Don't bypass the property system - use attribute assignment
- **Component lifecycle**: Understand `ComponentMeta` instantiation process
- **Session management**: Components must be created within session context
- **Import organization**: Use relative imports within Rio codebase
### Testing
- **Frontend tests**: Require built assets and browser setup
- **Component tests**: Focus on state changes and user interactions
- **Coverage**: Use `scripts/code_coverage.py` for comprehensive reporting
- **Mock dependencies**: Isolate components from external systems

View File

@@ -1,6 +1,6 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,14 +1,14 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.7
rev: v0.14.0
hooks:
- id: ruff
args: [check, --select, F401, --select, I, --fix]
- id: ruff-check
args: [--select, F401, --select, I, --fix]
- id: ruff-format
args: [--config, pyproject.toml]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
rev: v3.0.3
hooks:
- id: prettier
exclude: '\.md$'

259
AGENTS.md Normal file
View File

@@ -0,0 +1,259 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Rio is a Python-first web framework for building websites and apps entirely in Python (no HTML/CSS/JavaScript required). It uses a React-like component model with a hybrid architecture:
- **Python backend**: Component definitions, logic, state management, server communication
- **TypeScript frontend**: DOM rendering, event handling, client interactions
- **Communication**: WebSocket/HTTP RPC between Python and TypeScript
## Development Commands
### Initial Setup
```bash
# Install Python dependencies (required first)
uv sync --all-extras
# Install frontend dependencies
npm install
# Build frontend (REQUIRED before first use and after TypeScript changes)
npm run build
# Install pre-commit hooks
python -m pre_commit install
```
### Building
```bash
# Development build (default)
npm run dev-build
# Production build (for releases)
npm run build
```
**When to rebuild frontend**: After any changes to TypeScript/SCSS files in `/frontend/`, before testing, and before releases.
### Testing
```bash
# Run all Python tests
uv run pytest
# Run specific test file
uv run pytest tests/test_components.py
# Run with coverage
uv run scripts/code_coverage.py
# Frontend integration tests (requires browser setup)
uv run pytest tests/test_frontend/
```
### Code Quality
```bash
# Lint Python code
python -m ruff check .
# Format Python code
python -m ruff format .
# Format TypeScript code
npx prettier --write frontend/
# Run all pre-commit hooks
pre-commit run --all-files
```
### Running Rio Apps
```bash
# Create new project from template
rio new
# Run project in development mode
rio run
# Run in browser vs window
app.run_in_browser() # Web app
app.run_in_window() # Local desktop app
```
## Architecture
### Component System
Rio uses a dual implementation pattern for components:
**Python Side** (`/rio/components/`):
- Component class definitions with state/logic
- Observable properties that auto-trigger re-renders on change
- `build()` method for high-level components (compose from other components)
- `_custom_serialize_()` for sending state to frontend
- `_on_message_()` for handling frontend events
**TypeScript Side** (`/frontend/code/components/`):
- `ComponentBase` subclasses that create/update DOM elements
- `createElement()`: Build initial DOM structure
- `updateElement(deltaState)`: Apply state changes from Python
- Event handlers that send messages back to Python
**Component Hierarchy**:
```
Component (metaclass: ComponentMeta)
├── FundamentalComponent (maps to TypeScript/HTML, low-level)
│ └── Examples: Button, Text, ListView, Image
└── User Components (high-level, composed via build())
└── Examples: Custom business logic components
```
### Observable Property System
Rio uses `ComponentMeta` metaclass to transform component classes into observable dataclasses:
- Properties automatically track dependencies
- Reading a property during `build()` registers a dependency
- Changing a property triggers rebuild of dependent components
- State synchronization happens automatically between Python ↔ TypeScript
### Session Management
- Each client connection = one `Session` instance (`rio/session.py`)
- Session maintains: component tree, active page, user settings, timezone, window dimensions
- Components created within session context get auto-registered
- WebSocket communication for real-time updates
## Project Structure
### Python Backend
- `/rio/components/` - Built-in component implementations
- `/rio/app.py` - Core `App` class and application setup
- `/rio/session.py` - Session state and lifecycle management
- `/rio/cli/` - Command-line interface (rio new, rio run, etc.)
- `/rio/observables/` - Observable property system and state tracking
- `/rio/transports/` - WebSocket and HTTP transport layers
- `/rio/component_meta.py` - Metaclass for component creation
### TypeScript Frontend
- `/frontend/code/components/` - Component rendering implementations
- `/frontend/code/rpc.ts` - RPC communication with Python backend
- `/frontend/code/componentManagement.ts` - Component lifecycle and tree management
- `/frontend/css/components/` - Component-specific SCSS styles
- `/frontend/index.html` - Frontend entry point
### Build System
- `/vite.config.mjs` - Vite build configuration
- Output: `/rio/frontend files/` (auto-generated, included in wheel)
- Build process compiles TypeScript + SCSS → bundled assets
### Testing
- `/tests/test_components.py` - Component behavior tests
- `/tests/test_frontend/` - Browser-based integration tests
- `/tests/test_cli/` - CLI command tests
- `/tests/test_observables.py` - State management tests
### Scripts
- `/scripts/build_icon_set.py` - Build icon sets from SVGs
- `/scripts/code_coverage.py` - Generate coverage reports
- `/scripts/publish.py` - Release/publishing automation
- `/scripts/generate_stubs.py` - Type stub generation
## Development Workflow
### Adding New Built-in Components
1. Create Python component in `/rio/components/my_component.py`
2. Create TypeScript component in `/frontend/code/components/myComponent.ts`
3. Add SCSS styles in `/frontend/css/components/myComponent.scss`
4. Export from `/rio/components/__init__.py`
5. Add tests in `/tests/test_components.py`
6. Build frontend: `npm run dev-build`
### Making Frontend Changes
Frontend changes require rebuilding since TypeScript compiles to bundled assets:
```bash
# After editing .ts or .scss files
npm run dev-build
```
### Component Lifecycle
When a component is instantiated (`ComponentMeta.__call__`):
1. Session injection: `component._session_ = currently_building_session`
2. ID assignment: `component._id_ = session._next_free_component_id`
3. Registration in session's component registry
4. Property tracking setup for dependency management
5. Key registration for component reuse/reconciliation
## Code Conventions
### Naming
- **Event handlers**: Present tense (`on_change`, `on_press`, not `on_changed`)
- **Boolean properties**: Affirmative (`is_visible`, not `is_hidden`)
- **Dictionaries**: `keys_to_values` pattern (e.g., `ids_to_instances`)
- **Units**: SI base units (seconds, not milliseconds) unless unit in name
- **Python**: `snake_case` for variables/functions, `CamelCase` for classes
- **TypeScript**: `camelCase` for variables/functions, `UpperCamelCase` for classes
- **Files**: `snake_case` (Python), `camelCase` (TypeScript)
### Imports
Prefer importing modules, not values:
```python
# Good
import traceback
traceback.print_exc()
# Avoid
from traceback import print_exc
```
Exceptions for common imports:
```python
from __future__ import annotations # Always first
from datetime import datetime, timezone, timedelta
from pathlib import Path
import typing as t
import typing_extensions as te
import numpy as np
import pandas as pd
```
### Type Hints
- Use type hints extensively (helps users, type checkers, and IDE autocomplete)
- Rio is fully type-safe, which is a core feature
- Use `typing` module with `t.` alias
- Use `typing_extensions as te` for newer features
## Testing Guidelines
- Tests must work with built frontend assets (run `npm run dev-build` if frontend tests fail)
- Component tests focus on state changes and user interactions
- Use `uv run pytest` to run tests in proper environment
- Coverage reporting via `uv run scripts/code_coverage.py`
- Frontend tests require browser setup (Playwright)
## Pre-commit Hooks
Automatically run on commit:
- Ruff: Python linting and formatting
- Prettier: TypeScript/CSS/JSON formatting
## Common Pitfalls
### Frontend Development
- Always rebuild after TypeScript changes (`npm run dev-build`)
- Component state sync: Ensure `_custom_serialize_()` matches TypeScript expectations
- Each FundamentalComponent needs unique class name and `_unique_id_`
### Python Development
- Don't bypass observable property system - use normal attribute assignment
- Components must be created within session context
- Understand `ComponentMeta` instantiation process for advanced work
### Build Process
- Frontend assets are compiled into `/rio/frontend files/` and included in Python wheel
- Changes to `/frontend/` don't take effect until rebuild
- Production builds use minification and compression

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

View File

@@ -2,18 +2,44 @@
## unreleased
Changes:
- Components that depend on session attributes (like `active_page_url`) or
session attachments are now automatically rebuilt when those values change
- Components are now rebuilt immediately after a state change, not just after
event handlers
- `TextStyle` no longer has default values, omitted values are left unchanged
instead
Deprecations:
- The `CursorStyle` enum is deprecated in favor of string literals
Bugfixes:
- Fix `rio.Revealer` not stretching its child
- Fix various components propagating click events
Additions:
- `NumberInput` can now evaluate math expressions
- `PointerEventListener` can now listen to only specific button events
- `KeyEventListener` can now listen to only specifc hotkeys
- `Calendar` now has a `is_sensitive` parameter
- New component: `rio.PdfViewer`
- The `CursorStyle` enum is deprecated in favor of string literals
- `rio.Image` now has a loading animation
- Added `accessibility_role` parameter to all components
- Added 'accessibility_relationship' parameter to `Link` component
- Added `auto_focus` parameter to input components
- Added `Session.pick_folder`
- Added `Font.from_google_fonts` and `Font.from_css_file`
- Added `rio.HttpOnly`
- Added `text_style` parameter to `rio.TextInput`
- Experimental additions:
- `rio.List`
- `rio.Dict`
- `rio.Set`
- `rio.Dataclass`
## 0.11

View File

@@ -29,7 +29,7 @@ export class ImageComponent extends ComponentBase<ImageState> {
this.imageElement = document.createElement("img");
this.imageElement.role = "img";
// Dragging prevents pointer events and is annoying in general, so
// disable those.
// we'll disable that.
this.imageElement.draggable = false;
element.appendChild(this.imageElement);
@@ -93,8 +93,9 @@ export class ImageComponent extends ComponentBase<ImageState> {
private _updateSize(): void {
if (this.element.classList.contains("rio-loading")) {
// While loading a new image, the size is set to 100%. Don't
// overwrite it.
// While loading a new image, the size is set to 100%
this.imageElement.style.removeProperty("width");
this.imageElement.style.removeProperty("height");
return;
}

View File

@@ -10,6 +10,18 @@ export function stopPropagation(event: Event): void {
event.stopPropagation();
}
export function markLeftButtonAsHandled(event: PointerEvent): void {
if (event.button === 0) {
markEventAsHandled(event);
}
}
export function stopLeftButtonPropagation(event: PointerEvent): void {
if (event.button === 0) {
event.stopPropagation();
}
}
export abstract class EventHandler {
component: ComponentBase;

View File

@@ -1,6 +1,11 @@
import { TextStyle } from "./dataModels";
import { applyTextStyleCss, textStyleToCss } from "./cssUtils";
import { markEventAsHandled, stopPropagation } from "./eventHandling";
import {
markEventAsHandled,
stopPropagation,
stopLeftButtonPropagation,
markLeftButtonAsHandled,
} from "./eventHandling";
export type InputBoxStyle = "underlined" | "rounded" | "pill";
@@ -137,18 +142,24 @@ export class InputBox {
this.focus();
});
this.outerElement.addEventListener("pointerdown", stopPropagation);
this.outerElement.addEventListener("pointerup", stopPropagation);
// Consider any clicks on the input box as handled. This prevents e.g.
// drag events when trying to select something.
this.outerElement.addEventListener(
"pointerdown",
stopLeftButtonPropagation
);
this.outerElement.addEventListener(
"pointerup",
stopLeftButtonPropagation
);
this.prefixTextElement.addEventListener(
"pointerdown",
markEventAsHandled
markLeftButtonAsHandled
);
this.suffixElementContainer.addEventListener(
"pointerdown",
markEventAsHandled
markLeftButtonAsHandled
);
// When clicked, focus the text element and move the cursor accordingly.
@@ -180,7 +191,10 @@ export class InputBox {
// Pointer down events select the input element and/or text in it (via
// dragging), so let them do their default behavior but then stop them
// from propagating to other elements
this._inputElement.addEventListener("pointerdown", stopPropagation);
this._inputElement.addEventListener(
"pointerdown",
stopLeftButtonPropagation
);
}
private _hasDefaultHandler(event: KeyboardEvent): boolean {

View File

@@ -8,12 +8,12 @@ authors = [
]
dependencies = [
"crawlerdetect>=0.1.7,<0.4",
"fastapi>=0.110,<0.119",
"fastapi>=0.110,<0.120",
"gitignore-parser>=0.1.11,<0.2",
"identity-containers>=1.0.2,<2.0",
"imy[docstrings,deprecations]>=0.7.1,<0.8",
"introspection>=1.11.1,<2.0",
"isort>=5.13,<7.0",
"isort>=5.13,<8.0",
"langcodes>=3.4,<4.0",
"multipart>=1.2,<2.0",
"narwhals>=1.12,<3.0",
@@ -28,7 +28,7 @@ dependencies = [
"tomlkit>=0.12,<0.14",
"typing-extensions>=4.5,<5.0",
"unicall>=0.2post0,<0.3",
"uniserde>=0.4,<0.5",
"uniserde==0.4.1rc0",
"uvicorn[standard]>=0.29.0,<0.38",
"watchfiles>=0.21,<2.0",
"yarl>=1.9,<2.0",
@@ -73,7 +73,7 @@ classifiers = [
[project.optional-dependencies]
window = [
"aiofiles>=24.1,<25.0",
"aiofiles>=24.1,<26.0",
"copykitten>=1.2,<3.0",
"pywebview[pyside6]>=6.0,<7.0",
# Workaround for https://github.com/rio-labs/rio/issues/235
@@ -107,7 +107,7 @@ dev = [
"pyfakefs>=5.7.3,<6.0",
"pytest-cov>=5.0,<8.0",
"pytest>=8.2.1,<9.0",
"ruff>=0.9.9,<0.14",
"ruff>=0.9.9,<0.15",
]
[tool.hatch.version]

View File

@@ -133,9 +133,9 @@ class UvicornWorker:
Replace the app currently running in the server with a new one. The
worker must already be running for this to work.
"""
assert (
app_server.internal_on_app_start is None
), app_server.internal_on_app_start
assert app_server.internal_on_app_start is None, (
app_server.internal_on_app_start
)
# Store the new app
self.app_server = app_server

View File

@@ -36,9 +36,9 @@ class WebViewWorker:
assert self.window is None, "Already running"
# Make sure this was called from the main thread.
assert (
threading.current_thread() is threading.main_thread()
), "Must be called from the main thread"
assert threading.current_thread() is threading.main_thread(), (
"Must be called from the main thread"
)
# Fetch the icon
icon_path = asyncio.run(initial_app._fetch_icon_as_png_path())

View File

@@ -398,5 +398,12 @@ OnResizeMethodVar = t.TypeVar(
def on_resize(handler: OnResizeMethodVar) -> OnResizeMethodVar:
"""
Triggered whenever the component's size changes.
## Metadata
`decorator`: True
"""
_tag_as_event_handler(handler, EventTag.ON_RESIZE, None)
return handler

View File

@@ -1052,8 +1052,8 @@ window.resizeTo(screen.availWidth, screen.availHeight);
try:
error = task.exception()
except asyncio.CancelledError as e:
error = e
except asyncio.CancelledError:
return
if error is not None:
revel.error("Background task crashed:")

View File

@@ -1,5 +1,3 @@
import typing as t
import rio
# Define a theme for Rio to use.

View File

@@ -1,9 +1,9 @@
import rio
import pandas as pd
# <additional-imports>
import plotly.graph_objects as go
import pandas as pd
import rio
from .. import data_models

View File

@@ -34,14 +34,12 @@ def create_blobs_variants(blob: bytes) -> t.Iterable[bytes | t.IO[bytes]]:
yield f
def make_test_blobs() -> (
t.Iterable[
tuple[
bytes,
bytes | t.IO[bytes],
]
def make_test_blobs() -> t.Iterable[
tuple[
bytes,
bytes | t.IO[bytes],
]
):
]:
"""
Generate a variety of blobs to use as file contents, in different forms.
"""
@@ -58,15 +56,13 @@ def make_test_blobs() -> (
yield as_bytes, as_some_blob
def make_test_texts() -> (
t.Iterable[
t.Tuple[
bytes | t.IO[bytes],
str | None,
str | t.Type[Exception],
],
]
):
def make_test_texts() -> t.Iterable[
t.Tuple[
bytes | t.IO[bytes],
str | None,
str | t.Type[Exception],
],
]:
"""
Generate a variety of blobs to be decoded into strings, in different formats.
Each entry contains the blob, the encoding to use, and the expected output.

View File

@@ -3,11 +3,12 @@ import asyncio
import rio.testing
async def test_list() -> None:
pressed = []
async def test_change_selection_mode() -> None:
num_presses = 0
def on_press():
pressed.append(1)
nonlocal num_presses
num_presses += 1
async with rio.testing.BrowserClient(
lambda: rio.ListView(
@@ -22,7 +23,7 @@ async def test_list() -> None:
list_view = test_client.get_component(rio.ListView)
item = test_client.get_component(rio.SimpleListItem)
assert len(pressed) == 1
assert num_presses == 1
assert list_view.selected_items == [item.key]
list_view.selection_mode = "none"
@@ -31,7 +32,7 @@ async def test_list() -> None:
await test_client.wait_for_refresh()
await test_client.click(10, 1)
assert len(pressed) == 1
assert num_presses == 1
assert list_view.selected_items == []
list_view.selection_mode = "single"
@@ -39,5 +40,5 @@ async def test_list() -> None:
await test_client.wait_for_refresh()
await test_client.click(10, 1)
assert len(pressed) == 2
assert num_presses == 2
assert list_view.selected_items == [item.key]

View File

@@ -112,3 +112,57 @@ async def test_specific_button_events(
else:
assert len(down_events) == 0
assert len(up_events) == 0
@pytest.mark.parametrize("pressed_button", ["left", "middle", "right"])
@pytest.mark.parametrize(
"build_child, child_eats_press_event, child_eats_pointer_events",
[
(rio.Spacer, False, []),
(rio.TextInput, True, ["left"]),
],
)
async def test_event_propagation(
pressed_button: t.Literal["left", "middle", "right"],
build_child: t.Callable[[], rio.Component],
child_eats_press_event: bool,
child_eats_pointer_events: t.Sequence[str],
) -> None:
press_event: rio.PointerEvent | None = None
down_event: rio.PointerEvent | None = None
up_event: rio.PointerEvent | None = None
def on_press(e: rio.PointerEvent):
nonlocal press_event
press_event = e
def on_pointer_down(e: rio.PointerEvent):
nonlocal down_event
down_event = e
def on_pointer_up(e: rio.PointerEvent):
nonlocal up_event
up_event = e
def build():
return rio.PointerEventListener(
build_child(),
on_press=on_press,
on_pointer_down=on_pointer_down,
on_pointer_up=on_pointer_up,
)
async with BrowserClient(build) as client:
await client.click(0.5, 0.5, button=pressed_button, sleep=0.5)
if pressed_button == "left" and not child_eats_press_event:
assert press_event is not None
else:
assert press_event is None
if pressed_button not in child_eats_pointer_events:
assert down_event is not None
assert up_event is not None
else:
assert down_event is None
assert up_event is None