Integrate blitz-dom headless renderer and refactor document model#9
Merged
kevincarlson merged 27 commits intoMay 4, 2026
Conversation
…a GPU readback
Path B (CPU readback → data URI) was chosen because register_texture takes
ownership of wgpu::Texture, which conflicts with PageCache's ownership model.
- loki-render-cache: add data_uri: Option<Arc<String>> to CachedPage and
set_data_uri() to PageCache; add readback.rs (gpu feature) with
texture_to_data_uri() that copies each rendered texture to CPU memory and
encodes it as a PNG data URI; wire readback into render_queue worker after
every Rerender and Downsample job; add image/base64 deps behind gpu feature
- loki-renderer: rewrite DocumentView to snapshot cache state into a Vec
before rsx! (dropping the MutexGuard before the Dioxus diffing pass), then
render img { src } with the data URI when available, or a blank placeholder
while the first settle render is still in progress
https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Replace the RenderQueue + CPU-readback path (Session 7) with Blitz's native CustomPaintSource compositing model. Removed: - loki-render-cache/src/render_queue.rs — background render thread is wrong; CustomPaintCtx is frame-scoped and cannot escape render() - loki-render-cache/src/readback.rs — CPU copy_texture_to_buffer path no longer needed - loki-render-cache/src/mock_texture.rs — only existed to support the removed readback path - Byte-budget eviction (evict_cold_overflow) from retier.rs — Blitz owns texture lifetime via TextureHandle; we no longer track bytes - texture/data_uri fields from CachedPage; image/base64 deps from Cargo Added: - loki-renderer/src/page_paint_source.rs — LokiPageSource implements CustomPaintSource, one instance per page. render() reads the current CacheTier, allocates a wgpu texture, paints via loki-vello, registers with Blitz, and reuses the handle when tier/generation/size unchanged. - PageCache::page_count_by_tier() for tracing - impl Default for PageCache Updated: - DocumentView: new PageTile sub-component calls use_wgpu exactly once per page (satisfies Dioxus hook-count invariant); canvas element carries the registered handle ID - RendererState::new() drops device/wgpu_queue params; on_settle() logs via page_count_by_tier() - PageCache::insert() now takes (index, tier) only; no texture arg Quality: cargo clippy -p loki-render-cache clean; 25/25 tests passing. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Replace the hardcoded `current_generation = 0u64` in LokiPageSource with a real generation signal so Loro mutations trigger page re-renders. Option B chosen: DocPageSource holds Arc<AtomicU64> (starts at 1). DocumentState is in loki-text which loki-renderer does not depend on, so Arc<Mutex<DocumentState>> would require an undesirable cross-crate coupling. The atomic gives callers a simple advance_generation() handle with no new crate dependencies. Changes: - doc_page_source.rs: replace OnceLock<PaginatedLayout> with Mutex<Option<(u64, PaginatedLayout)>>; add generation: Arc<AtomicU64>; add current_generation(), advance_generation(), layout_for_generation(). Generation starts at 1 so LokiPageSource (texture_generation = 0) always renders on first frame. Check + recompute are atomic under one lock. - page_paint_source.rs: replace `current_generation = 0u64` with `self.source.current_generation()`; replace `self.source.layout()` with `layout_for_generation(current_generation)`. Guard dropped before render_to_texture to minimise lock hold time. - renderer_state.rs: replace layout() with layout_for_generation; drop guard before acquiring cache lock to avoid two simultaneous locks. - document_view.rs: same pattern; guard dropped before rsx! macro. Quality: loki-render-cache clippy clean; 25/25 tests passing. loki-renderer clippy unverifiable (pre-existing fontique build failure). https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Root cause: yeslogic-fontconfig-sys 6.0.0 removed direct symbol exports from the static-linkage path — FcCharSetAddChar and friends are now only defined inside dlib::external_library!() which is gated on the dlopen feature. The fontconfig-dlopen feature was not included in the system feature bundle so the #[cfg(not(feature = "fontconfig-dlopen"))] block in patches/fontique/src/backend/fontconfig.rs compiled and failed with 28 unresolved import errors. Fix: add "fontconfig-dlopen" to the system feature in both patches/fontique/Cargo.toml and Cargo.toml.orig. fontconfig 2.15.0 is present on this system so dlopen resolution succeeds at runtime. Also rename `gen` variable (reserved keyword in Rust 2024 edition) to `doc_gen` in renderer_state.rs, document_view.rs, and doc_page_source.rs — these were masked by the fontique failure until now. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
One warning in loki-renderer itself: - clippy::question_mark in page_paint_source.rs:191 — let...else returning None rewritten with ? operator Third-party dep warnings (loki-layout: select_header/select_footer dead_code) are out of scope for this crate's clippy pass; addressed in Task D workspace sweep. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
No binary #[ignore] tests exist in loki-renderer. The 3 ignored entries reported by cargo test are doc-tests in /// ```ignore blocks: - renderer_state.rs:11 — use_hook / provide_context snippet; requires Dioxus component context; correctly ignored - doc_page_source.rs:91 — layout_for_generation usage example; can't run standalone; correctly ignored - scroll_driver.rs:45 — use_drop cancellation snippet; requires Dioxus async executor; correctly ignored All three are intentional and require no change. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
…loki-opc loki-opc (1 fix): - part_names.rs: collapse nested if into if && let (clippy::collapsible_if) loki-layout (36 fixes across flow.rs, flow_para.rs, para.rs, resolve.rs, lib.rs): - dead_code: remove select_header/select_footer - collapsible_if: collapse if/if-let chains with && - useless_conversion: remove .into() on String/&str - extend→append: use Vec::append for full drain - loop_variable: use iter_mut().skip().take() instead of index - is_multiple_of: replace x % n == 0 with .is_multiple_of(n) - ptr_arg: &mut Vec<T> → &mut [T] where only iteration is needed - redundant_closure: |x| f(x) → f - clone_on_copy: remove .clone() on Copy types (LayoutColor) - map_or_is_some_and: .map_or(false, f) → .is_some_and(f) - doc_list_item_without_indentation: add 2-space indent in resolve.rs - assign_op_pattern: x = x + y → x += y loki-odf (partial — 8 of 76 fixes; remaining in follow-up commit): - unused imports in lib.rs, export.rs, mod.rs - missing backticks in constants.rs - package.rs: partial cleanup https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
…_integration test
loki-odf — 67 warnings fixed across 8 files:
- unnested_or_patterns (3): flatten nested Some("a") | Some("b") → Some("a"|"b")
- match_same_arms (15): merge End(_)|Eof identical break arms with |
- manual_let_else / single_match_else (6): rewrite match-None-return as let…else
- map_unwrap_or (3): .map().unwrap_or(false) → .is_some_and(); .map_or(d, f)
- collapsible_if (5): collapse nested if/if-let chains with &&
- field_reassign_with_default (2): use struct initializer with ..Default::default()
- cast_lossless (2): u8 as u16 → u16::from(u8)
- cast_possible_wrap (1): as i32 → .cast_signed()
- redundant_closure (2): |v| v.into_owned() → Cow::into_owned
- ref_option (1): &Option<String> params → Option<&String>
- useless_conversion (4): remove identity quick_xml::Error::from(e)
- too_many_lines (3): #[allow] with comment on XML event-loop functions
- single_match (1): match → if local == ...
- missing_errors_doc (2): add # Errors sections
- len_zero (1): archive.len() == 0 → archive.is_empty()
- case_sensitive_file_extension_comparisons (6): use .eq_ignore_ascii_case()
- items_after_statements (1): move use to top of function
- consider_adding_semicolon (1): add trailing semicolons
loki-render-cache/tests/gpu_integration.rs:
- Rewrote stale test that referenced removed RenderQueue, PageCache::new(budget),
and RetierResult.evicted from pre-Session-8 architecture
- New tests: retier_marks_pages_for_rerender (metadata path, no GPU needed) +
allocate_and_downsample_texture_succeeds (GPU path, skips if no adapter)
https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
… complete
loki-ooxml (147 → 0 warnings across 25 files):
- cast_lossless: i32 as f64 → f64::from(i32)
- unnested_or_patterns: flatten nested Some("a")|Some("b") → Some("a"|"b")
- collapsible_if (14): collapse if/if-let chains with &&
- missing_backticks_in_doc (11): wrap identifiers in backticks
- match_same_arms (10): merge identical arms with |
- map_or / is_some_and (7): simplify map().unwrap_or patterns
- redundant_closure (7): |x| f(x) → f
- manual_let_else (5): match-None-return → let…else
- needless_pass_by_value (2): T param → &T where value not consumed
- large_enum_variant (2): #[allow] with comment (boxing adds alloc overhead)
- field_reassign_with_default (2): struct initializer with ..Default::default()
- cast_precision_loss (2): #[allow] with comment (precision acceptable for measurements)
- wildcard_imports (1): expand import
- unnecessary_wrapping (1): Option<T> return → T
- too_many_lines (1): #[allow] with comment (XML event-loop function)
- assigning_clones (2): use clone_from or direct assignment
- binding_too_similar (4): rename shadowing variables
- dead_code: #[allow] on model structs not yet wired to output pipeline
loki-text (10 warnings across 6 files):
- question_mark (2): if is_none / match → ?
- collapsible_if (3): collapse nested if/if-let with &&
- deref_addrof (1): remove explicit * deref
- too_many_arguments (1): #[allow] with comment on hit_test_document
- clone_on_copy (2): remove .clone() on Signal/Navigator (both Copy)
- iter_last (1): filter().next_back() → rfind()
Final state:
cargo clippy --workspace --no-deps -- -D warnings → 0 errors
cargo test --workspace → 0 failures
https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
20-page document was allocating 20 × vello::Renderer — one per PageCanvas (LokiDocumentSource) — consuming ~2.83 GB. vello::Renderer is stateless between calls (holds compiled shaders and staging buffers tied to the Device, not per-page data), so all pages for a document can share one instance. Changes: - Add `shared_renderer: Arc<Mutex<Option<vello::Renderer>>>` to `DocumentState` so all `LokiDocumentSource` instances for the same document share one Arc. - `LokiDocumentSource` clones the Arc from `DocumentState` at construction; the per-instance `renderer: Option<vello::Renderer>` field is removed. - `resume()`: first source to run creates the renderer under a lock; the remaining N-1 sources find it already initialised and skip Renderer::new(). - `suspend()`: sets the shared renderer to None (idempotent; first caller does the real drop, subsequent callers are no-ops). - `render()` Phase 5: locks the shared renderer instead of using as_mut(). Sequential render loop in anyrender_vello (values_mut()) means no contention. - Early-return guard simplified: device/queue check retained; renderer initialisation failure is now handled gracefully via the Option::as_mut()? return at the Phase 5 lock site. Memory reduction: 20-page doc 20 × 141 MB → 1 × 141 MB for the renderer. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
…yout via DocumentState Each LokiDocumentSource previously owned a CachedLayout holding a full DocumentLayout (PaginatedLayout + FontDataCache). For a 20-page document this was 20 independent copies of the same data, ~17 MB/copy = ~340 MB total. The fix stores exactly one PaginatedLayout in DocumentState.paginated_layout (Arc<PaginatedLayout>) and shares it across all page sources. Changes: - Remove CachedLayout struct and layout_cache field from LokiDocumentSource - Remove needs_relayout() method — staleness detection moves to DocumentState - Add layout_stamp/layout_generation/layout_canvas_width/layout_preserve_for_editing to DocumentState: bumped whenever the layout is recomputed, enabling per-source texture reuse guards to detect staleness without per-source layout copies - Add shared_font_cache: Arc<Mutex<FontDataCache>> to DocumentState (same Arc pattern as shared_renderer); glyphs loaded while painting page 0 are reused for pages 1-N on the same frame; cache reset on any layout recompute - render() Phase 2: the first source to find the layout stale recomputes and writes to DocumentState; all others on the same frame read the fresh copy. Blitz sequential execution makes this race-free without extra synchronisation. - render() reuse guard: texture_stamp (was texture_generation) now tracks layout_stamp instead of doc generation; added texture_preserve_effective to detect cursor-mode changes that require re-layout without a generation bump - Phase 4: paint_single_page receives shared_font_cache via Arc lock instead of per-source FontDataCache; paint_layout removed (always paginated) - suspend(): drop layout_cache line removed; texture_stamp/texture_preserve_effective cleared alongside handle/size Memory reduction: 20 × CachedLayout (DocumentLayout + FontDataCache, ~17 MB each) → 1 × Arc<PaginatedLayout> + 1 × Arc<Mutex<FontDataCache>>. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Each LokiDocumentSource previously created its own FontResources in resume(), allocating a parley::FontContext (full Fontique font database scan) and a parley::LayoutContext (shaping scratch space) per page canvas — 20 instances for a 20-page document at ≈20 MB each ≈ 400 MB. parley::LayoutContext docs: "designed to be a global resource, one per application." All fields are scratch buffers overwritten at the start of each layout call, so sharing under a Mutex is safe. Blitz sequential rendering means no contention. Changes: - Remove font_resources: Option<FontResources> from LokiDocumentSource - Add shared_font_resources: Arc<Mutex<FontResources>> to DocumentState; initialised with one FontResources::new() at DocumentState construction - LokiDocumentSource::new clones the Arc (same pattern as shared_renderer) - resume(): remove FontResources::new() call — nothing left to do for fonts - render() Phase 2: lock shared_font_resources for layout_document, release before writing results back to DocumentState - apply_mutation_and_relayout: eliminate local FontResources::new() (the 21st instance, created on every keystroke); clone shared_font_resources Arc from DocumentState instead, drop the doc_state lock before acquiring the font resources lock to avoid lock-ordering deadlock Memory reduction: 20 × FontResources (≈400 MB) → 1 × FontResources. Expected total after all three sessions: ~320 MB for a 20-page document. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Implements Session 3b-1: block-level structural mutations for the Enter
key (split) and Backspace-at-offset-zero (merge) editing flows.
- Refactor loro_mutation.rs → loro_mutation/{mod,text,block}.rs (file was
over 300 lines after additions)
- Add MutationError variants: InvalidByteOffset { offset } and NoPreviousBlock
- split_block: splits LoroText at a UTF-8 byte boundary, inserts a new
LoroMap into the LoroMovableList, copies KEY_TYPE / KEY_PARA_PROPS /
KEY_DIRECT_CHAR_PROPS from the source block
- merge_block: appends block N's text to block N-1's LoroText, removes
block N from the list, returns the byte-offset join point for cursor
placement
- 15 new unit tests covering split middle/start/end/Unicode/invalid-
boundary/OOB, merge basic/offset/at-zero/OOB/removes-block, and a
split→merge round-trip
- Export split_block and merge_block from loki-doc-model crate root
https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Session 3b-2: implements all five navigation/editing stubs in editor.rs. Navigation logic extracted to editing/navigation.rs (pure layout functions, no Dioxus/Loro dependencies) to keep editor.rs as a thin event dispatcher. New editing/navigation.rs: - navigate_left / navigate_right: grapheme boundary movement; cross-paragraph on same page (verified via PageParagraphData::block_index lookup) - navigate_up / navigate_down: cursor_rect → canvas-local coords → hit_test_page; cross-page deferred (TODO(3b-3)) - navigate_home / navigate_end: cursor_rect y-center → hit_test_point at x=0 or x=100_000 pt on the same line - 8 unit tests covering left/right at boundaries, home/end, up/down at edges editor.rs changes: - ArrowLeft/Right: call navigate_left/right with get_block_text closure; Shift+Arrow extends selection (focus only), plain Arrow collapses both - ArrowUp/Down: call navigate_up/down; same Shift semantics - Home/End: call navigate_home/navigate_end - Enter: call split_block + apply_mutation_and_relayout; cursor moves to (paragraph+1, offset 0); TODO(3b-3) for page_index recompute after split - Backspace at offset 0: call merge_block + apply_mutation_and_relayout; cursor moves to (paragraph-1, merged_offset) - All doc_state locks use unwrap_or_else(|e| e.into_inner()) for poison safety https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Keyboard events were not reaching the onkeydown handler because the scroll container's tabindex="0" only receives focus when its background is clicked directly — not when child canvases (which have no tabindex) receive the click. Fix: add on_keydown: EventHandler<Rc<KeyboardData>> prop to WgpuSurface and move tabindex="0"/onkeydown to WgpuSurface's outer wrapper div so that the element that receives pointer clicks also holds keyboard focus. Handler logic stays in editor.rs and is forwarded up via the prop. Rc<KeyboardData> is used because KeyboardData does not implement Clone; Rc<T>: Clone provides cheap shared ownership of the event data. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
…andler Temporary diagnostic to trace whether keyboard events reach the on_keydown handler after the tabindex focus fix. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
handle_click walked up the DOM from the hit element but called clear_focus() for any element that wasn't input/label/a. This meant clicking a wgpu canvas (or any non-input child) unconditionally cleared keyboard focus, preventing onkeydown from ever reaching a parent div with tabindex="0". Fix: read is_focussable() before the element data borrow each iteration. After the match arm, if the node is focusable, call set_focus_to() and return instead of continuing the walk. The clear_focus() at the bottom is only reached when no focusable ancestor exists at all. Vendors blitz-dom 0.2.4 to patches/blitz-dom for this change. Remove when upstream blitz-dom implements tabindex click-to-focus for non-input elements. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Temporary println! probes at every layer to pinpoint where the click→ focus→keydown chain breaks: - patches/blitz-dom/src/events/mouse.rs: "BLITZ FOCUS: setting focus to node N" when handle_click finds a focusable ancestor - wgpu_surface.rs outer div: OUTER DIV ONCLICK, OUTER DIV ONFOCUS, OUTER DIV ONKEYDOWN — distinguishes whether focus lands but keyboard events don't reach the Dioxus handler - editor.rs on_keydown: "KEY: ..." at the top before any guards Remove all println! once the root cause is confirmed. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Two new print layers to isolate stale-node hypothesis: - patches/blitz-dom/src/events/driver.rs: DRIVER_KEYDOWN prints focussed_node_id and whether the node still exists in the DOM tree at the moment KeyDown is processed. Stale node → re-render replaced the outer div after set_focus_to was called. - patches/dioxus-native-dom/src/dioxus_document.rs: DIOXUS_HANDLER prints the full bubble chain for every KeyDown event, and for each node whether get_dioxus_id resolves to a Dioxus ElementId. A chain of nodes with no dioxus_id confirms the focused node no longer has data-dioxus-id (stale after re-render). https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Adds println! inside clear_focus() itself to prove it's being called and identify whether it fires between set_focus_to and the next KeyDown. Expected output sequence after click + key: BLITZ FOCUS: setting focus to node N OUTER DIV ONCLICK CLEAR_FOCUS: clearing focus from node N ← where does this come from? DRIVER_KEYDOWN: focussed_node_id=None, ... Once the CLEAR_FOCUS line appears we know exactly which code path calls it (handle_click end-of-walk, or somewhere else). https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
When a mouse click triggers an on_mousedown handler that updates a signal, Dioxus re-renders the component tree during the next poll(). This replaced the focused blitz node (e.g. the outer tabindex="0" div) with a new node ID, and the subsequent handle_click default action walked a stale tree and called clear_focus(). Fix by snapshotting the focused node's stable Dioxus ElementId before render_immediate() and re-applying set_focus_to() after render if the blitz node ID changed. Also exposes focus_node_id as pub on BaseDocument (in the blitz-dom patch) so dioxus-native-dom can read it. Remove all diagnostic println! statements added during debugging. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
Add numbered debug printlns to trace the full keyboard event path: - DBG0: poll() focus state before/after render_immediate - DBG1: focus_node_id seen at KeyDown dispatch time in driver - DBG2: whether on_keydown handler fires in editor, and what editor mode - DBG3: cursor_state.focus value at handler entry - DBG4: insert_text / apply_mutation_and_relayout result https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
flush_is_focussable() was only called in ElementData::new(). Dioxus's template renderer creates elements with Vec::new() (no attrs) then applies static attributes like tabindex="0" via set_attribute(), so is_focussable was always left as false for template-created elements. handle_click()'s tabindex-focusable check therefore never fired, and every canvas click fell through to clear_focus(). Fix: call flush_is_focussable() in set_attribute() and clear_attribute() when tabindex or disabled changes. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
loro_to_document() produces Document::new() with an empty StyleCatalog because the catalog is not stored in the CRDT. apply_mutation_and_relayout() was therefore rendering with no styles on the first keystroke, causing the document to lose all formatting (fonts, paragraph spacing, headings, etc.). Fix: before re-computing the layout, copy styles and source from the previously stored document in doc_state into the reconstructed document. The catalog is effectively read-only during an editing session, so copying it from the last good state is safe. Also remove all DBG0-DBG4 diagnostic println!s now that the focus and formatting bugs are resolved. https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
…lization
Replace Debug-string stubs in map_para_props/map_loro_block with correct
encode/decode helpers so direct_para_props and direct_char_props on
StyledParagraph survive a CRDT round-trip.
Serialization changes:
- ParagraphAlignment: variant name string ("Left", "Center", …)
- Spacing: "Exact:<pts_f64>" / "Percent:<pct_f32>"
- LineHeight: "Exact:<pts_f64>" / "AtLeast:<pts_f64>" / "Multiple:<mul_f32>"
- ListId: bare inner string (previously format!("{:?}", …) which wrapped
it in 'ListId("…")')
- direct_char_props: flat LoroMap using bool/f64 for scalar fields and
Debug strings for UnderlineStyle/StrikethroughStyle/VerticalAlign
Deserialization additions:
- reconstruct_para_props(): reads KEY_PARA_PROPS sub-map back into ParaProps
- reconstruct_char_props_from_map(): reads KEY_DIRECT_CHAR_PROPS sub-map
back into CharProps
- Helper accessors (get_str/f64/bool/i64_from_map) used by both functions
tab_stops and color fields are intentionally deferred (complex types with
no clean round-trip format yet).
https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
… round-trips - Split loro_bridge.rs (769 lines) into mod.rs, write.rs, read.rs, inlines.rs - Add PageLayout serialization: page_size, margins, orientation, columns - Add six HeaderFooter slots (header/footer/first/even) with recursive block content - Add full inline mark reconstruction: underline, font_size, font_family, letter_spacing, word_spacing, vertical_align, link_url, small_caps, etc. - Add clean tagged-string encode/decode for Spacing and LineHeight enums - Add schema constants for layout, margins, columns, and header/footer keys - Add 8 new round-trip tests covering all newly supported features https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
…coding
Replace Debug-format string encoding for color fields with proper encode/decode:
- DocumentColor::Rgb → to_hex() / from_hex() (#RRGGBB); non-Rgb variants
(Theme, Cmyk, Transparent) are gracefully dropped with a TODO comment
- HighlightColor → Debug-format names ("Yellow" etc.) with a new
decode_highlight_color() match table for the full palette
- Add background_color to the map path (write.rs / read.rs) — was missing
- Wire color and highlight_color decode in read_char_props_from_marks
- 3 new tests: Rgb color survives, Transparent drops gracefully, Yellow
highlight survives
https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR integrates the blitz-dom headless DOM renderer into the Loki document suite and refactors the document model layer to support bidirectional Loro CRDT serialization. The changes enable rendering documents through a layout engine while maintaining CRDT-based collaborative editing capabilities.
Key Changes
Blitz-DOM Integration
patches/blitz-dom/) with headless DOM implementation including:document.rs,node/) with style and layout supportlayout/) with taffy-based box model, inline layout, tables, and listsevents/) for mouse, keyboard, and IME inputform.rs,net.rs)assets/default.css)Document Model Refactoring
loki-doc-model::loro_bridgefrom single file to modular architecture:loro_bridge/mod.rs— public API (document_to_loro,loro_to_document)loro_bridge/write.rs— serialization (Loki → Loro)loro_bridge/read.rs— deserialization (Loro → Loki)loro_bridge/inlines.rs— inline content handlingloro_mutation.rstoloro_mutation/mod.rswith submodules:loro_mutation/text.rs— character-level mutationsloro_mutation/block.rs— block-level structural mutations (split/merge)loro_to_documentfunctionEditor Navigation
loki-text/src/editing/navigation.rswith cursor navigation functions:navigate_left,navigate_right,navigate_up,navigate_downnavigate_home,navigate_endRender Cache Refactoring
PageCacheto track tier and dirty metadata instead of GPU texturesRenderQueueandmock_texture.rs(GPU rendering now handled externally)retier.rsto focus on tier reassignment logicDocPageSourceto bridge layout cache between document model and page sourceComponent Updates
DocumentStateinloki-text/components/document_source.rsto useArc<PaginatedLayout>DocumentViewto integrate withPageCacheand GPU renderingeditor.rsrouteTest Updates
loro_bridge_tests.rswith comprehensive serialization/deserialization testsloro_mutation_tests.rsto cover block-level mutationsloki-render-cacheMinor Fixes
twips_to_pt,f64::from)#[allow(dead_code)]annotations for internal model typesrustc-hashdependency toloki-doc-modelImplementation Details
PaginatedLayout)DocumentMutatorhttps://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d