Headings in rendered (WYSIWYG) mode use RenderedEditSession for buffer ownership, one-click block switching, and commit-on-switch — replacing legacy rendered_focus defer/commit hacks for this block type.
PRD: Rendered Edit Session
Foundation: Core types, Widget identity
| Action | Result |
|---|---|
| Edit heading A, click heading B | A commits to source, B activates with one click |
| Click mid-word in heading | Cursor placed via galley mapping; stable while typing |
| Rendered heading edit commits | Source updated via update_source_line; source_epoch unchanged |
| Focus leaves heading (no other session block) | close_active_ui(SaveIfDirty) commits buffer |
| Concern | Location |
|---|---|
| Session load/save per frame | show_rendered_editor — rendered_session::load / save(ui, editor_id, …) |
| Heading render | render_heading in src/markdown/editor.rs |
| Block identity | BlockRef::Heading { line, structural } — structural selects heading_text vs heading_text_sk widget id |
| TextEdit id | block_ref.widget_id(ui) under push_id(editor_id) + push_id(source_epoch) |
| Buffer | BlockEditState.text — cold init from node.text_content() |
| Activation | session.switch_to_ui + PendingActivation { cursor_char_index, request_focus } |
| Text edits | session.on_text_changed (marks dirty; no source write) |
| Commit | Callback: format_heading + update_source_line; level from # prefix on source line |
Click/focus on heading B while A is active
→ switch_to_ui(B, PendingActivation { … })
→ commit callback writes A buffer if dirty
→ surrender A egui focus
→ B buffer active; pending_activation applied after TextEdit show
Cross-widget click uses heading_click_cursor (wraps compute_displayed_cursor_index) so the caret lands at the click position.
- Temp egui buffers:
heading_edit_buffer,heading_sk_edit_buffer, tracking ids rendered_focus::after_text_editfor headingsrendered_focus::focus_loss_should_commitfor headings- Separate
render_heading_with_structural_keys— unifiedrender_heading(..., structural: bool)
Paragraphs, formatted blocks, list items, and tables all use the session — see linked docs below.
RenderedEditSession is loaded once in show_rendered_editor and passed as &mut through render_node and container helpers (blockquote, callout) so nested headings share the same session.
rendered_session::— state machine, widget id suffix stabilitytest_heading_level_from_source,test_rendered_heading_widget_id_*ineditor.rstests module
- Document with
# Alphaand# Beta - Edit Alpha, single-click Beta → Alpha saved, Beta focused
- Click mid-word in a heading, type several seconds → cursor stays put