Skip to content

Integrate blitz-dom headless renderer and refactor document model#9

Merged
kevincarlson merged 27 commits into
claude/improve-loki-documentation-rfjR6from
claude/virtual-scroll-positioning-cXMOT
May 4, 2026
Merged

Integrate blitz-dom headless renderer and refactor document model#9
kevincarlson merged 27 commits into
claude/improve-loki-documentation-rfjR6from
claude/virtual-scroll-positioning-cXMOT

Conversation

@kevincarlson

Copy link
Copy Markdown
Member

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

  • Added complete blitz-dom crate (patches/blitz-dom/) with headless DOM implementation including:
    • Core document model (document.rs, node/) with style and layout support
    • Layout engine (layout/) with taffy-based box model, inline layout, tables, and lists
    • Event handling system (events/) for mouse, keyboard, and IME input
    • Form handling and stylesheet loading (form.rs, net.rs)
    • Font metrics provider for text measurement
    • Default HTML/CSS stylesheet (assets/default.css)

Document Model Refactoring

  • Restructured loki-doc-model::loro_bridge from 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 handling
  • Moved loro_mutation.rs to loro_mutation/mod.rs with submodules:
    • loro_mutation/text.rs — character-level mutations
    • loro_mutation/block.rs — block-level structural mutations (split/merge)
  • Added bidirectional document conversion support with loro_to_document function

Editor Navigation

  • Added loki-text/src/editing/navigation.rs with cursor navigation functions:
    • navigate_left, navigate_right, navigate_up, navigate_down
    • navigate_home, navigate_end
    • Cross-page navigation support with clamping at page boundaries

Render Cache Refactoring

  • Simplified PageCache to track tier and dirty metadata instead of GPU textures
  • Removed RenderQueue and mock_texture.rs (GPU rendering now handled externally)
  • Updated retier.rs to focus on tier reassignment logic
  • Refactored DocPageSource to bridge layout cache between document model and page source

Component Updates

  • Updated DocumentState in loki-text/components/document_source.rs to use Arc<PaginatedLayout>
  • Modified DocumentView to integrate with PageCache and GPU rendering
  • Added navigation imports to editor.rs route

Test Updates

  • Enhanced loro_bridge_tests.rs with comprehensive serialization/deserialization tests
  • Updated loro_mutation_tests.rs to cover block-level mutations
  • Simplified GPU integration tests in loki-render-cache

Minor Fixes

  • Fixed type conversions in OOXML/ODF mappers (e.g., twips_to_pt, f64::from)
  • Added #[allow(dead_code)] annotations for internal model types
  • Updated documentation comments across multiple modules
  • Added rustc-hash dependency to loki-doc-model

Implementation Details

  • Blitz-DOM uses Servo's style engine for CSS parsing and computed values, with taffy for layout
  • Document mutations are tracked through damage flags for incremental layout
  • Loro CRDT containers are mapped to document structure with schema keys for metadata, styles, and content
  • Navigation functions work in layout points (shared coordinate space with PaginatedLayout)
  • Form submission and stylesheet loading are deferred through an operation queue in DocumentMutator

https://claude.ai/code/session_0152a892tWygTUHBHRHJTE6d

claude added 27 commits May 3, 2026 14:04
…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
@kevincarlson kevincarlson merged commit 999e109 into claude/improve-loki-documentation-rfjR6 May 4, 2026
@kevincarlson kevincarlson deleted the claude/virtual-scroll-positioning-cXMOT branch May 4, 2026 12:47
@AppThere AppThere locked as resolved and limited conversation to collaborators May 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants