# Refactor Turbo Renderer State Management to Use RxJS
### TL;DR
Refactored the TurboRenderer state management to use RxJS observables instead of direct state mutations, improving state transitions and reactivity.
### What changed?
- Replaced the public `state` property with a private `state$` BehaviorSubject in `ViewportTurboRendererExtension`
- Added proper state transition logging using RxJS operators
- Combined multiple event subscriptions using `merge` operator for better organization
- Improved state transition logic in the `refresh()` method
- Updated the `zooming$` and `panning$` signals in the Viewport
### Why make this change?
This refactoring improves the codebase by:
1. Using a more consistent reactive programming model with RxJS
2. Making state transitions more explicit and traceable
3. Reducing potential bugs from manual state management
4. Improving code organization by combining related event streams
5. Ensuring proper cleanup of resources when components are disposed
The change maintains the same functionality while making the code more maintainable and the state management more robust.
### What Changes
- Fixed incorrect edgeless viewport display in peek view
- Moved page block viewport fit animation logic from `EdgelessRootBlockComponent` to note config extension
- Disabled page block viewport fit animation in peek view, using default `fitToScreen` instead
- @doodlewind Fixed viewport resizing issues by adding a immediate update mechanism to ensure proper rendering during peek view operations. The setViewportByBound is only called once during peek view initialization, so there are barely perf overhead.
- Updated related test cases
- Refactored peek view test cases to make them clearer and more reliable
- Added new test helper function `getViewportBound` for getting viewport boundary information
continue #10824
### Changed
- Moved double-click-to-edit behavior from the default tool to individual model views
- Introduced `onSelected` callback interface in gfx view components to allows developers to override default selection logic
### Overview:
We've been working with some legacy code in the default-tool and edgeless-selected-rect modules, which are responsible for fundamental operations like moving, resizing, and rotating elements. Currently, these operations are hardcoded, making it challenging to extend functionalities without diving deep into the code.
### What's Changing:
Introducing `ElementTransformManager` to streamline the handling of basic transformations (move, resize, rotate) while allowing the business logic to dictate when these actions occur.
Providing two ways to extend the transformations behaviour:
- Extends inside element view definition: Elements can decide how to handle move/resize events, such as enforcing size constraints.
- Extension mechanism provided by this manager: Adjust or completely override default drag behaviors, like snapping elements into alignment.
### Code Examples:
Delegate element movement to TransformManager:
```typescript
class DefaultTool {
override dragStart(event) {
if(this.dragType === DragType.ContentMoving) {
const transformManager = this.std.get(TransformManagerIdentifier);
transformManager.startDrag({ selectedElements, event });
}
}
}
```
Enforce minimum width inside view definition:
```typescript
class EdgelessNoteBlock extends GfxBlockComponent {
onResizeDelta({ dw, dh }) {
const bound = this.model.elementBound;
bound.w = Math.min(MAX_WIDTH, bound.w + dw);
bound.h = Math.min(MAX_HEIGHT, bound.h + dh);
this.model.xywh = bound.serialize();
}
}
```
Use extension to implement element snapping:
```typescript
import { TransformerExtension } from '@blocksuite/std/gfx';
// Just extends the TransformerExtension
class SnapManager extends TransformerExtension {
static override key = 'snap-manager';
onDragInitialize() {
return {
onDragMove(context) {
const { dx, dy } = this.getAlignmentMoveDistance(context.elements);
context.dx = dx;
context.dy = dy;
}
}
}
}
```
### Others
The migration will be divided into several PRs. This PR mostly focus on refactoring elements movement part of `default-tool`.
- Delegate elements movement to `TransformManager`
- Rewrite the default tool extension into `TransformManager` extension
- Add drag handler interface to gfx view (both `GfxBlockComponent` and `GfxElementModelView`) to allow element to define how it gonna react on drag
Currently, `GfxViewportElement` hides DOM blocks outside the viewport using `display: none` to optimize performance. However, this approach presents two issues:
1. Even when hidden, all top-level blocks still undergo frequent CSS transform updates during viewport panning and zooming.
2. Hidden blocks cannot access DOM layout information, preventing `TurboRenderer` from updating the complete canvas bitmap.
To address this, this PR introduces a refactoring that divides all top-level edgeless blocks into two states: `idle` and `active`. The improvements are as follows:
1. Blocks outside the viewport are set to the `idle` state, meaning they no longer update their DOM during viewport panning or zooming. Only `active` blocks within the viewport are updated frame by frame.
2. For `idle` blocks, the hiding method switches from `display: none` to `visibility: hidden`, ensuring their layout information remains accessible to `TurboRenderer`.
[Screen Recording 2025-03-07 at 3.23.56 PM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/lEGcysB4lFTEbCwZ8jMv/4bac640b-f5b6-4b0b-904d-5899f96cf375.mov" />](https://app.graphite.dev/media/video/lEGcysB4lFTEbCwZ8jMv/4bac640b-f5b6-4b0b-904d-5899f96cf375.mov)
While this minimizes DOM updates, it introduces a trade-off: `idle` blocks retain an outdated layout state. Since their positions are updated using a lazy update strategy, their layout state remains frozen at the moment they were last moved out of the viewport:

To resolve this, the PR serializes and stores the viewport field of the block at that moment on the `idle` block itself. This allows the correct layout, positioned in the model coordinate system, to be restored from the stored data.
Close [BS-1869](https://linear.app/affine-design/issue/BS-1869/[bug]-android-chrome-%E8%BE%93%E5%85%A5%E9%94%99%E8%AF%AF)
## Problem
On Android devices, keyboard events do not properly capture key information, causing the backspace key and other keyboard functionalities to malfunction. This is due to the specific behavior of Android platform, as discussed in:
- https://stackoverflow.com/a/68188679
- https://stackoverflow.com/a/66724830
## Solution
1. Added special handling for Android platform in `KeyboardControl` class by using `beforeInput` event instead of `keyDown` event
2. Implemented `androidBindKeymapPatch` function to handle special key events on Android platform
3. Updated event handling logic in related components, including:
- CodeBlock
- ListKeymap
- ParagraphKeymap
- PageKeyboardManager
## Changes
- Added `androidBindKeymapPatch` function for handling key events on Android platform
- Modified `KeyboardControl.bindHotkey` method to add `beforeInput` event handling for Android
- Unified event object access using `ctx.get('defaultState').event` instead of `keyboardState.raw`
- Updated key event handling logic in multiple components
## Before
https://github.com/user-attachments/assets/e8602de4-d584-4adf-816f-369f38312022
## After
https://github.com/user-attachments/assets/f9e1680e-28ff-4d52-bdab-7683cdcb6f82
The main changes in this PR involve replacing the deprecated `BlockServiceWatcher` with the new `LifeCycleWatcher` across multiple files. Here's a detailed breakdown:
1. **Core Architectural Change:**
- Removed `BlockServiceWatcher` class completely (deleted file)
- Migrated to `LifeCycleWatcher` as the new standard for watching component lifecycle events
2. **Key Changes in Implementation:**
- Changed from using `blockService.specSlots` events to using `view.viewUpdated` events
- Replaced `flavour` static property with `key` static property
- Updated event handling to use more specific payload type checking
3. **Major File Changes:**
- Modified multiple block components:
- Embed synced doc block
- Frame preview
- Edgeless root spec
- AI-related components (code, image, paragraph, etc.)
- Quick search service
- Edgeless clipboard
4. **Pattern of Changes:**
The migration follows a consistent pattern:
```typescript
// Old pattern
class SomeWatcher extends BlockServiceWatcher {
static override readonly flavour = 'some:flavour';
mounted() {
this.blockService.specSlots.viewConnected.on(...)
}
}
// New pattern
class SomeWatcher extends LifeCycleWatcher {
static override key = 'some-watcher';
mounted() {
const { view } = this.std;
view.viewUpdated.on(payload => {
if (payload.type !== 'block' || payload.method !== 'add') return;
// Handle event
});
}
}
```
5. **Benefits:**
- More explicit and type-safe event handling
- Cleaner architecture by removing deprecated code
- More consistent approach to lifecycle management
- Better separation of concerns
This appears to be a significant architectural improvement that modernizes the codebase by removing deprecated patterns and standardizing on a more robust lifecycle management system.
1. **Major Architectural Change: Schema Management**
- Moved from `workspace.schema` to `store.schema` throughout the codebase
- Removed schema property from Workspace and Doc interfaces
- Added `BlockSchemaExtension` pattern across multiple block types
2. **Block Schema Extensions Added**
- Added new `BlockSchemaExtension` to numerous block types including:
- DataView, Surface, Attachment, Bookmark, Code
- Database, Divider, EdgelessText, Embed blocks (Figma, Github, HTML, etc.)
- Frame, Image, Latex, List, Note, Paragraph
- Root, Surface Reference, Table blocks
3. **Import/Export System Updates**
- Updated import functions to accept `schema` parameter:
- `importHTMLToDoc`
- `importHTMLZip`
- `importMarkdownToDoc`
- `importMarkdownZip`
- `importNotionZip`
- Modified export functions to use new schema pattern
4. **Test Infrastructure Updates**
- Updated test files to use new schema extensions
- Modified test document creation to include schema extensions
- Removed direct schema registration in favor of extensions
5. **Service Layer Changes**
- Updated various services to use `getAFFiNEWorkspaceSchema()`
- Modified transformer initialization to use document schema
- Updated collection initialization patterns
6. **Version Management**
- Removed version-related properties and methods from:
- `WorkspaceMetaImpl`
- `TestMeta`
- `DocImpl`
- Removed `blockVersions` and `workspaceVersion/pageVersion`
7. **Store and Extension Updates**
- Added new store extensions and adapters
- Updated store initialization patterns
- Added new schema-related functionality in store extension
This PR represents a significant architectural shift in how schemas are managed, moving from a workspace-centric to a store-centric approach, while introducing a more extensible block schema system through `BlockSchemaExtension`. The changes touch multiple layers of the application including core functionality, services, testing infrastructure, and import/export capabilities.
### Changed
- Add new image block to render dnd preview
- Fixed the bug that dragging uploaded image does not have width and height
- Fixed the bug that drag image block from page to edgeless does not have width and height
- Better edgeless dnd preview