mirror of
https://github.com/rio-labs/rio.git
synced 2026-02-11 08:10:29 -06:00
merge changes from main branch
This commit is contained in:
239
.github/copilot-instructions.md
vendored
239
.github/copilot-instructions.md
vendored
@@ -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
|
||||
|
||||
8
.github/dependabot.yml
vendored
8
.github/dependabot.yml
vendored
@@ -1,6 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
@@ -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
259
AGENTS.md
Normal 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
|
||||
28
changelog.md
28
changelog.md
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:")
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import typing as t
|
||||
|
||||
import rio
|
||||
|
||||
# Define a theme for Rio to use.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user