Running text: paragraphs of verses with inline numbers (#12 part 3c) #115

Merged
claude-bot merged 11 commits from feat/running-text into main 2026-10-29 15:28:02 +00:00
Collaborator

Part of #12 (part 3c of the split in comment 2328).

What

A new Display setting, Running text (off by default), shows each paragraph as flowing text with small raised verse numbers inline, instead of one row per verse.

  • Paragraphs come from the module's own marks (3b). This PR also fixes their detection for BSB, which puts <div type="x-p"> inside its pre-verse headings.
  • Go to, Back, restore and the 100 ms place reports work by verse inside a paragraph. Switching modes keeps the top verse.

Blast radius

Tier: trunk
Trunk paths touched: sword/src/filter/osis.rs, core/src/bible* (new block.rs, chapter.rs, reader.rs), core/src/render.rs, core/src/worker.rs, core/src/worker/session/jump.rs, qt/src/reading_model.rs, qml/pages/ReadingPane.qml, tools/e2e/cases/running-text.sh
Testing tier: standard (logic and UI).
Gate / rollback: the text.running setting defaults to off. With it off, the pane is verse mode as before: the same StyledText delegate, now inside a Loader. To undo fully, revert this merge; the setting key is then ignored. There is no schema change.

Proof

  • CI on fb44892: changes, rust, audit, app, e2e and click.

  • CI on f8ba0c6 caught a broken E2E hook: red-letters saw 0 red verses with red on, because the delegate became a Loader and the list's children lost text. fb44892 gives the row text: model.text. Locally, E2E_CASES="red-letters red-letters-kill switch-bible-red-letters running-text" passes 4/4: 21 verses red with it on, 0 off. The f8ba0c6 run (job 2018) is the run without the fix.

  • cargo make passes locally with SWORD_TEST_PATH set: fmt, clippy, tests with coverage, release check, deny and audit.

  • cargo make app-docker passes. Mem-check PSS: 168028 kB normal, 169541 kB slow, against the 176000 baseline (verse mode).

  • E2E_CASES="running-text display-settings display-spacing restore" cargo make e2e-docker: 4 passed. The running-text case checks:

    • the toggle and the relayout;
    • the top verse kept both ways;
    • restore after a kill;
    • line pitch equal to verse mode's (running 22, verse 22).
  • Fails without the change: each new test fails under a deliberate bug and passes once restored.

    test mutation fails at
    block plain_length_drops_tags_and_counts_entities_once len_utf16() → 1 block.rs:130
    block a_verse_block_is_the_verse start offset 0 → 1 block.rs:137
    block running_text_joins_verses_with_the_start_of_each inter-verse space not counted block.rs:148
    block numbers_are_inline_and_counted no-break space not counted block.rs:167
    block a_verse_without_text_starts_where_the_next_does empty verse at at + 1 block.rs:181
    chapter verse_mode_is_a_block_per_verse !running inverted chapter.rs:96
    chapter running_text_breaks_at_paragraph_starts para_start inverted chapter.rs:102
    chapter running_text_without_paragraph_marks_is_one_block i > from guard dropped chapter.rs:108
    render running_text_and_its_numbers_change_the_layout numbers-change clause dropped render.rs:184
    render layouts_colour_the_numbers_for_the_window colour in verse mode too render.rs:198
    worker running_text_loads_the_chapters_again_at_the_top_verse relayout only when switching on worker.rs:194 (no Loaded)
    osis paragraph_starts_come_before_the_first_text (real BSB 13:18 markup) pre-verse check removed / x-p div not matched osis.rs:370 / osis.rs:375
    reader marks_paragraph_starts (BSB Matt 13 has 18 and 24) the osis fix reverted reader.rs:186 ([1])
    e2e running-text pitch × 1.3 running 28.6 verse 22, case failed
  • Phone (arm64 click, ute2e, comments 2376, 2381 and 2394). The owner passed the checklist on e0a9032: feel, flick, tap and looks. I re-checked c06603a and 3835f97 myself:

    • Numbers are clear of the line above in Sans and Serif (full-resolution crops).
    • Pitch is 24/24 in Sans and 23/23 in Serif.
    • BSB Matt 13 has 73 paragraph rows (was 3).
    • Go to 13:21 mid-paragraph, then Back to 13:16.
    • Kill and relaunch restores 24540.
  • Memory: verse mode is unchanged (mem-check above). Running mode on the phone peaks at 70926 kB PSS against 70272 kB in verse mode, after 20 flings each in the same session (+0.65 MB, a TextEdit per paragraph). No new start-up work: blocks are built only in running mode, from rows already loaded.

Review

Reviewer:

  • Fresh-context review at high on 199e13d (review 317): 1 medium, 1 low.

    • Medium: the ruler's text readback always differed, so the guard never applied. Fixed in 6375bad; the ruler was later removed in 70255da.
    • Low: the spacing of a log line. Fixed in 6375bad.
  • Re-review at medium on 6375bad (318): no findings.

  • Medium review of 199e13d..3835f97 (319), covering the phone fixes: 2 low.

    • The doc comment named the wrong markup. Fixed in f8ba0c6.
    • The pitch-log timing. Refuted in comment 2398: both values are synchronous, and every run logged non-zero.
  • Delta review of f8ba0c6..fb44892 (the E2E hook fix): review 320, no findings.

Nothing is deferred or left open.
Review-Confidence: 75/100. A trunk change of 768 lines was reviewed at high and re-reviewed three times, with CI green and fails-without-change evidence for every new test. Review 319 missed one regression: the Loader dropped the rows' text for the red-letters E2E hook. CI caught it and fb44892 fixed it. So trust the E2E and phone proofs as much as the review.

Confidence by part

  • Block and offset arithmetic (block.rs, chapter.rs): high. Unit tests are mutation-checked, and the phone checks of Go to, Back and restore pass.
  • Paragraph detection (osis.rs): high for KJV and BSB, the tested modules. Other modules with unusual markup may still differ.
  • Running-mode QML (TextEdit, pitch, <sup><font size="-1">): medium. It was checked on CI's Qt and on the phone in two fonts and one text size. Other text sizes were only measured in CI at 20 px.
  • Worker relayout on toggle: high (worker test plus e2e).

🤖 Generated with Claude Code

Part of #12 (part 3c of the split in comment 2328). ## What A new Display setting, **Running text** (off by default), shows each paragraph as flowing text with small raised verse numbers inline, instead of one row per verse. - Paragraphs come from the module's own marks (3b). This PR also fixes their detection for BSB, which puts `<div type="x-p">` inside its pre-verse headings. - Go to, Back, restore and the 100 ms place reports work by verse inside a paragraph. Switching modes keeps the top verse. ## Blast radius **Tier:** trunk **Trunk paths touched:** `sword/src/filter/osis.rs`, `core/src/bible*` (new `block.rs`, `chapter.rs`, `reader.rs`), `core/src/render.rs`, `core/src/worker.rs`, `core/src/worker/session/jump.rs`, `qt/src/reading_model.rs`, `qml/pages/ReadingPane.qml`, `tools/e2e/cases/running-text.sh` **Testing tier:** standard (logic and UI). **Gate / rollback:** the `text.running` setting defaults to off. With it off, the pane is verse mode as before: the same StyledText delegate, now inside a Loader. To undo fully, revert this merge; the setting key is then ignored. There is no schema change. ## Proof - **CI on fb44892:** changes, rust, audit, app, e2e and click. - **CI on f8ba0c6 caught a broken E2E hook:** `red-letters` saw 0 red verses with red on, because the delegate became a Loader and the list's children lost `text`. fb44892 gives the row `text: model.text`. Locally, `E2E_CASES="red-letters red-letters-kill switch-bible-red-letters running-text"` passes 4/4: 21 verses red with it on, 0 off. The f8ba0c6 run (job 2018) is the run without the fix. - **`cargo make`** passes locally with SWORD_TEST_PATH set: fmt, clippy, tests with coverage, release check, deny and audit. - **`cargo make app-docker`** passes. Mem-check PSS: 168028 kB normal, 169541 kB slow, against the 176000 baseline (verse mode). - **`E2E_CASES="running-text display-settings display-spacing restore" cargo make e2e-docker`:** 4 passed. The `running-text` case checks: - the toggle and the relayout; - the top verse kept both ways; - restore after a kill; - line pitch equal to verse mode's (`running 22, verse 22`). - **Fails without the change:** each new test fails under a deliberate bug and passes once restored. | test | mutation | fails at | |---|---|---| | block `plain_length_drops_tags_and_counts_entities_once` | `len_utf16()` → 1 | block.rs:130 | | block `a_verse_block_is_the_verse` | start offset 0 → 1 | block.rs:137 | | block `running_text_joins_verses_with_the_start_of_each` | inter-verse space not counted | block.rs:148 | | block `numbers_are_inline_and_counted` | no-break space not counted | block.rs:167 | | block `a_verse_without_text_starts_where_the_next_does` | empty verse at `at + 1` | block.rs:181 | | chapter `verse_mode_is_a_block_per_verse` | `!running` inverted | chapter.rs:96 | | chapter `running_text_breaks_at_paragraph_starts` | `para_start` inverted | chapter.rs:102 | | chapter `running_text_without_paragraph_marks_is_one_block` | `i > from` guard dropped | chapter.rs:108 | | render `running_text_and_its_numbers_change_the_layout` | numbers-change clause dropped | render.rs:184 | | render `layouts_colour_the_numbers_for_the_window` | colour in verse mode too | render.rs:198 | | worker `running_text_loads_the_chapters_again_at_the_top_verse` | relayout only when switching on | worker.rs:194 (no Loaded) | | osis `paragraph_starts_come_before_the_first_text` (real BSB 13:18 markup) | pre-verse check removed / `x-p` div not matched | osis.rs:370 / osis.rs:375 | | reader `marks_paragraph_starts` (BSB Matt 13 has 18 and 24) | the osis fix reverted | reader.rs:186 (`[1]`) | | e2e `running-text` | pitch × 1.3 | `running 28.6 verse 22`, case failed | - **Phone** (arm64 click, ute2e, comments 2376, 2381 and 2394). The owner passed the checklist on e0a9032: feel, flick, tap and looks. I re-checked c06603a and 3835f97 myself: - Numbers are clear of the line above in Sans and Serif (full-resolution crops). - Pitch is 24/24 in Sans and 23/23 in Serif. - BSB Matt 13 has 73 paragraph rows (was 3). - Go to 13:21 mid-paragraph, then Back to 13:16. - Kill and relaunch restores 24540. - **Memory:** verse mode is unchanged (mem-check above). Running mode on the phone peaks at 70926 kB PSS against 70272 kB in verse mode, after 20 flings each in the same session (+0.65 MB, a TextEdit per paragraph). No new start-up work: blocks are built only in running mode, from rows already loaded. ## Review **Reviewer:** - Fresh-context review at high on 199e13d (review 317): 1 medium, 1 low. - Medium: the ruler's `text` readback always differed, so the guard never applied. Fixed in 6375bad; the ruler was later removed in 70255da. - Low: the spacing of a log line. Fixed in 6375bad. - Re-review at medium on 6375bad (318): no findings. - Medium review of 199e13d..3835f97 (319), covering the phone fixes: 2 low. - The doc comment named the wrong markup. Fixed in f8ba0c6. - The pitch-log timing. Refuted in comment 2398: both values are synchronous, and every run logged non-zero. - Delta review of f8ba0c6..fb44892 (the E2E hook fix): review 320, no findings. Nothing is deferred or left open. **Review-Confidence:** 75/100. A trunk change of 768 lines was reviewed at high and re-reviewed three times, with CI green and fails-without-change evidence for every new test. Review 319 missed one regression: the Loader dropped the rows' `text` for the red-letters E2E hook. CI caught it and fb44892 fixed it. So trust the E2E and phone proofs as much as the review. ## Confidence by part - Block and offset arithmetic (`block.rs`, `chapter.rs`): high. Unit tests are mutation-checked, and the phone checks of Go to, Back and restore pass. - Paragraph detection (`osis.rs`): high for KJV and BSB, the tested modules. Other modules with unusual markup may still differ. - Running-mode QML (TextEdit, pitch, `<sup><font size="-1">`): medium. It was checked on CI's Qt and on the phone in two fonts and one text size. Other text sizes were only measured in CI at 20 px. - Worker relayout on toggle: high (worker test plus e2e). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A chapter's rows become blocks (a verse each, or a paragraph from each
para_start verse) with each verse's start in the plain text, so a position
maps to a verse. Toggling running text reloads at the anchor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
QML measures with a hidden TextEdit (Text has no position lookup) and the
core maps position to verse; the saved place is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
e2e: running-text case; docs F4 (#12 part 3c)
Some checks failed
CI / changes (pull_request) Successful in 13s
CI / rust (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
199e13ddc1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-review requested changes 2026-10-29 13:02:18 +00:00
Dismissed
claude-review left a comment

Fresh-context review, level: high (trunk). Reviewed commit: 199e13ddc1. Range: origin/main...HEAD.

Files reviewed: core/src/bible.rs, core/src/bible/block.rs, core/src/bible/chapter.rs, core/src/bible/reader.rs, core/src/render.rs, core/src/settings.rs, core/src/worker.rs, core/src/worker/session.rs (context), core/src/worker/session/jump.rs, qt/src/reading_model.rs, qml/pages/ReadingPane.qml, qml/pages/DisplaySettings.qml, tools/e2e/cases/running-text.sh, tools/e2e/lib.sh (helpers exist), docs/03-architecture.md, docs/04-features.md, po/de.po, po/utstudybible.schaefera.pot.

Checked and found sound: block/verse offset arithmetic (plain_len vs Qt positionAt for tags, entities, UTF-16, collapsed ASCII whitespace from sword::Run::collapse), Loaded row to ordinal to (row, offset) mapping, Span row counts for slide/drop, event ordering of Setting then Loaded on relayout (worker handles commands in order, ids stay above shown), Rerendered same-rows check, settings order and the start[7] test.

Findings outside the diff: none.

Inline: 1 medium, 1 low.

Fresh-context review, level: high (trunk). Reviewed commit: 199e13ddc1fd2f4afe7a4be2fd808ec713fbb616. Range: origin/main...HEAD. Files reviewed: core/src/bible.rs, core/src/bible/block.rs, core/src/bible/chapter.rs, core/src/bible/reader.rs, core/src/render.rs, core/src/settings.rs, core/src/worker.rs, core/src/worker/session.rs (context), core/src/worker/session/jump.rs, qt/src/reading_model.rs, qml/pages/ReadingPane.qml, qml/pages/DisplaySettings.qml, tools/e2e/cases/running-text.sh, tools/e2e/lib.sh (helpers exist), docs/03-architecture.md, docs/04-features.md, po/de.po, po/utstudybible.schaefera.pot. Checked and found sound: block/verse offset arithmetic (plain_len vs Qt positionAt for tags, entities, UTF-16, collapsed ASCII whitespace from sword::Run::collapse), Loaded row to ordinal to (row, offset) mapping, Span row counts for slide/drop, event ordering of Setting then Loaded on relayout (worker handles commands in order, ids stay above `shown`), Rerendered same-rows check, settings order and the start[7] test. Findings outside the diff: none. Inline: 1 medium, 1 low.
@ -23,2 +23,3 @@
console.info("utsb: display gap " + Theme.where(gap.sliderItem, gap.sliderItem.width - 8))
console.info("utsb: display red " + Theme.where(red.switchItem) + (red.enabled ? " on" : " off"))
console.info("utsb: display running " + Theme.where(running))
console.info("utsb: display red "+ Theme.where(red.switchItem) + (red.enabled ? " on" : " off"))
Collaborator

low: unrelated edit to the red log line (the space before + was dropped: "utsb: display red "+ Theme.where). qmlformat in the app job may flag this; restore the original line.

low: unrelated edit to the red log line (the space before `+` was dropped: `"utsb: display red "+ Theme.where`). qmlformat in the `app` job may flag this; restore the original line.
@ -349,0 +391,4 @@
ruler.width = width
ruler.font.pixelSize = font.pixelSize
ruler.font.family = font.family
if (ruler.text !== model.text)
Collaborator

medium: ruler.text !== model.text is always true. The ruler is textFormat: TextEdit.RichText, and in Qt 5 TextEdit.text returns document->toHtml() (a full <!DOCTYPE ...><html> document), never the string that was assigned. So the guard never skips and every ruled() call re-parses the whole paragraph and lays it out again. ruled() runs once per posAt, twice per verseSpan (both lineTop calls) and again in placeTop, so each 100 ms report in topVerse (posAt + verseSpan) does about three full HTML parses and layouts of the row. A module with no paragraph marks is one block per chapter (Ps 119, about 176 verses), so on the low-memory phones this targets, scrolling running text re-parses and re-lays out a chapter-sized document about 30 times a second. Cache the last assigned string in a plain property (property string rulerText) and compare against that, or set the text once per delegate (on text change) instead of in ruled().

medium: `ruler.text !== model.text` is always true. The ruler is `textFormat: TextEdit.RichText`, and in Qt 5 `TextEdit.text` returns `document->toHtml()` (a full `<!DOCTYPE ...><html>` document), never the string that was assigned. So the guard never skips and every `ruled()` call re-parses the whole paragraph and lays it out again. `ruled()` runs once per `posAt`, twice per `verseSpan` (both `lineTop` calls) and again in `placeTop`, so each 100 ms report in `topVerse` (posAt + verseSpan) does about three full HTML parses and layouts of the row. A module with no paragraph marks is one block per chapter (Ps 119, about 176 verses), so on the low-memory phones this targets, scrolling running text re-parses and re-lays out a chapter-sized document about 30 times a second. Cache the last assigned string in a plain property (`property string rulerText`) and compare against that, or set the text once per delegate (on `text` change) instead of in `ruled()`.
Author
Collaborator

Round 1 review, both findings fixed:

  • medium, ReadingPane.qml:394 (ruler guard never skips): right, a RichText TextEdit reads text back as HTML. The ruler now keeps an assigned string property and the guard compares against it, so the paragraph is parsed once per row change, not on every ruled() call.
  • low, DisplaySettings.qml:25 (space before +): restored.

app-docker green (PSS 163991 kB normal / 166594 kB slow, budget 176000 kB); e2e running-text and display-settings pass.

Round 1 review, both findings fixed: - medium, ReadingPane.qml:394 (ruler guard never skips): right, a RichText TextEdit reads `text` back as HTML. The ruler now keeps an `assigned` string property and the guard compares against it, so the paragraph is parsed once per row change, not on every ruled() call. - low, DisplaySettings.qml:25 (space before `+`): restored. app-docker green (PSS 163991 kB normal / 166594 kB slow, budget 176000 kB); e2e running-text and display-settings pass.
Pane: compare the ruler's text against what was assigned, not its HTML readback; restore a spacing slip (#12 part 3c)
All checks were successful
CI / changes (pull_request) Successful in 8s
CI / rust (pull_request) Successful in 6m7s
CI / audit (pull_request) Successful in 14s
CI / app (pull_request) Successful in 3m33s
CI / e2e (pull_request) Successful in 9m8s
CI / click (pull_request) Successful in 2m54s
6375bad33f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude-review left a comment

Re-review, level: medium (trunk). Reviewed commit: 6375bad33f. Range: 199e13ddc1fd2f4afe7a4be2fd808ec713fbb616..HEAD.

Files reviewed: qml/pages/ReadingPane.qml, qml/pages/DisplaySettings.qml (plus the ruler's other uses in ReadingPane.qml).

The range holds two small QML hunks. The ruler.assigned property is the only writer path to ruler.text. It is updated together with text, so the comparison no longer depends on the HTML readback. The DisplaySettings change is a spacing fix in a log string.

Findings: none (no defects with a concrete failure scenario).

Re-review, level: medium (trunk). Reviewed commit: 6375bad33f087a1be32d55d9a547b9787efa4282. Range: 199e13ddc1fd2f4afe7a4be2fd808ec713fbb616..HEAD. Files reviewed: qml/pages/ReadingPane.qml, qml/pages/DisplaySettings.qml (plus the ruler's other uses in ReadingPane.qml). The range holds two small QML hunks. The `ruler.assigned` property is the only writer path to `ruler.text`. It is updated together with `text`, so the comparison no longer depends on the HTML readback. The DisplaySettings change is a spacing fix in a log string. Findings: none (no defects with a concrete failure scenario).
Author
Collaborator

Phone check (installed 14:29, build feat/running-text@6375bad33)

The app starts cleanly on the phone (journal: window loaded after 350 ms, restored BSB Matt 13, no QML errors). Hold the footer to confirm the build line. Running text is under ⋮ › Settings › Display › Running text.

  • Toggle keeps the verse: switch Running text on and off a few times mid-chapter. The verse at the top stays the same.
  • Go to a mid-paragraph verse in running mode (e.g. KJV Gen 1:7): it lands with that verse at the top, not the paragraph's start.
  • Back after that Go to returns to the earlier spot.
  • Kill and restart in running mode (swipe the app away, reopen): same verse at the top.
  • KJV ¶ paragraphs: blocks start at the ¶ verses; a chapter start is always a new block.
  • A module without paragraph marks: one block per chapter, under its heading.
  • Inline verse numbers: bold and muted inside the paragraphs, readable in light and dark theme; red letters, italics and LORD still right.
  • Serif font in running mode looks right.
  • Scroll smoothness in running mode, including a long chapter (Ps 119 in a module without paragraphs). This is the least-proven part; CI didn't measure it.
  • Top-verse tracking where a verse starts mid-line: scroll slowly, then toggle back to verse per line; the verse shown is the one you were reading. QML measures with a hidden TextEdit and assumes its line breaks match the displayed Text; that assumption is unverified on the phone.

Reply here, or tell me in the session, with what passed and anything that looks off.

🤖 Generated with Claude Code

## Phone check (installed 14:29, build `feat/running-text@6375bad33`) The app starts cleanly on the phone (journal: window loaded after 350 ms, restored BSB Matt 13, no QML errors). Hold the footer to confirm the build line. Running text is under ⋮ › Settings › Display › **Running text**. - [x] **Toggle keeps the verse:** switch Running text on and off a few times mid-chapter. The verse at the top stays the same. - [x] **Go to a mid-paragraph verse** in running mode (e.g. KJV Gen 1:7): it lands with that verse at the top, not the paragraph's start. - [x] **Back** after that Go to returns to the earlier spot. - [x] **Kill and restart** in running mode (swipe the app away, reopen): same verse at the top. - [x] **KJV ¶ paragraphs:** blocks start at the ¶ verses; a chapter start is always a new block. - [x] **A module without paragraph marks:** one block per chapter, under its heading. - [x] **Inline verse numbers:** bold and muted inside the paragraphs, readable in light and **dark** theme; red letters, italics and LORD still right. - [x] **Serif** font in running mode looks right. - [x] **Scroll smoothness** in running mode, including a long chapter (Ps 119 in a module without paragraphs). *This is the least-proven part; CI didn't measure it.* - [x] **Top-verse tracking where a verse starts mid-line:** scroll slowly, then toggle back to verse per line; the verse shown is the one you were reading. QML measures with a hidden TextEdit and assumes its line breaks match the displayed Text; that assumption is unverified on the phone. Reply here, or tell me in the session, with what passed and anything that looks off. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Pane: running text is a RichText TextEdit, inline verse numbers small and raised (#12 part 3c)
Some checks failed
CI / changes (pull_request) Successful in 9s
CI / rust (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
70255da4fa
The paragraph row was a StyledText Text, which has no <sup> and whose
<font size> ignores the pane's pixel size, so the inline numbers were full
size; and a hidden ruler TextEdit measured it, assuming the same line
breaks. Now a running row is a read-only RichText TextEdit (the core emits
<sup><b><font color>n</font></b></sup>), drawn and measured by one layout;
the ruler is gone. Verse mode keeps its Text. Line spacing is the
document's CSS line-height percentage.

Memory/scroll: one TextEdit (QTextDocument) per visible paragraph row
instead of a Text plus a shared ruler document; the per-row cost is a
document layout instead of a StyledText layout, a few kB on a chapter's
rows, and mem-check (verse mode) is unchanged at 160211/162865 kB of 176000.
Scroll and restore e2e cases pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Phone check progress (owner, 2026-10-29):

  • 6375bad: inline verse numbers were full size → fixed in 70255da (RichText TextEdit rows, <sup> numbers). 70255da shows tight line spacing and raised numbers touching the line above; a fix is in progress.
  • Flicking and tapping on running-text rows feel the same as verse mode ✓
  • Screen reader: not testable on Ubuntu Touch; skipped by the owner, follow-up #118.

🤖 Generated with Claude Code

Phone check progress (owner, 2026-10-29): - 6375bad: inline verse numbers were full size → fixed in 70255da (RichText TextEdit rows, `<sup>` numbers). 70255da shows tight line spacing and raised numbers touching the line above; a fix is in progress. - Flicking and tapping on running-text rows feel the same as verse mode ✓ - Screen reader: not testable on Ubuntu Touch; skipped by the owner, follow-up #118. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Pane: running text's line pitch matches verse mode's; the raised number is smaller (#12 part 3c)
Some checks failed
CI / changes (pull_request) Successful in 11s
CI / rust (pull_request) Successful in 7m9s
CI / audit (pull_request) Successful in 16s
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
15860ca758
A RichText line-height percentage is of a line without leading, 3.7 % less
than the Text's at 22 px, so running lines were tighter. The TextEdit now
gets the pitch in pixels (FontMetrics.height x the setting), which matches
the Text's row height exactly at 100/130/200 % (standalone Qt 5 check in the
CI image: 189/245.7/378 px for seven lines of the same text in both). The
inline number is a <sup> whose span is 90 % of the size (0.6 in all, as verse
mode's number), so its raise stays below the line above's descenders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pane: running text's pitch is read from a verse-mode Text, smaller raised number (#12 part 3c)
Some checks failed
CI / changes (pull_request) Successful in 9s
CI / rust (pull_request) Successful in 7m43s
CI / audit (pull_request) Successful in 17s
CI / app (pull_request) Successful in 3m57s
CI / e2e (pull_request) Failing after 9m19s
CI / click (pull_request) Has been skipped
e0a9032515
The pitch came from FontMetrics.height, which on the phone's font is below
the Text's natural line. A hidden Text of verse mode's font and spacing now
gives the pitch whatever the font, and the first running row logs
'utsb: line pitch running X verse Y' (the running-text case asserts they
agree within 0.5 px; CI font: 22 and 22). The number's span is 80 % of the
sup (0.53 of the size, was 0.6) so its raise clears the descenders above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pane: the raised number stays inside its line, in any font (#12 part 3c)
Some checks failed
CI / changes (pull_request) Successful in 9s
CI / rust (pull_request) Successful in 2m57s
CI / audit (pull_request) Successful in 11s
CI / app (pull_request) Successful in 2m16s
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
c06603acda
RichText ignores font-size in percent, so the number was the sup's 2/3 and
its top 2-3 px above the line (Sans and Serif, measured by ink rows in a
standalone Qt 5 render in the CI image). <sup><font size="-1"> is about
0.55 of the text and its top is 2 px below the line's top in both, so it
cannot reach the descenders of the line above at spacing 100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paragraphs: BSB's x-p divs inside pre-verse matter start a paragraph (#12 part 3c)
Some checks failed
CI / changes (pull_request) Successful in 6s
CI / rust (pull_request) Has been cancelled
CI / audit (pull_request) Has been cancelled
CI / app (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / click (pull_request) Has been cancelled
3835f97b5f
BSB marks a paragraph with <div type="x-p" sID=..> after the section
headings, inside the x-preverse block the filter skips, so running text
ran whole chapters together (Matt 13: 3 rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Phone check of c06603a + 3835f97 (arm64 click, Volla), done with ute2e (taps, shots, journal). The owner had already passed the checklist on e0a9032 (feel, flick, tap).

  • Raised numbers (c06603a, <sup><font size="-1">). Full-resolution crops in Sans (ASV Ps 119:139) and Serif (same verse) show the number under a "g" descender of the line above, clear of it in both fonts. Line pitch is logged as running 24 verse 24 in Sans and 23/23 in Serif.
  • Paragraphs were missing for BSB (fixed in 3835f97). BSB Matt 13 in running mode was 3 rows for the whole window. BSB marks paragraphs as <div type="x-p" sID=…/> inside the pre-verse block after its headings, and the filter skipped that whole block and only knew div type="paragraph". Now Matt 13 starts paragraphs at 1, 3, 5, 7–11, 14, 16, 18, 20, 22–24, 27–29, … (the module's own marks), and the window has 73 rows. KJV is unchanged (Gen 1: 1, 6, 9, 14, 24, 26, 29).
    • Tests: filter::osis::paragraph_starts_come_before_the_first_text (the real BSB 13:18 markup) and reader::marks_paragraph_starts (BSB Matt 13 has 18 and 24). Both fail without each half of the fix: without the pre-verse check, osis.rs:370; without the x-p div match, osis.rs:375. cargo make passes.
  • Go to Matt 13:21 (mid-paragraph) puts 21's line at the top (saved 24545); Back returns to 13:16 (24540). Kill and relaunch restores BSB 24540 in running mode.
  • I found a separate bug in the ASV module, in both modes: words run together around added words ("allthyprecepts", "AndI"). The module itself has no space between </w><transChange>, so this is not caused by this PR. Filed as #119.

The phone is back to Sans and verse mode.

🤖 Generated with Claude Code

Phone check of c06603a + 3835f97 (arm64 click, Volla), done with ute2e (taps, shots, journal). The owner had already passed the checklist on e0a9032 (feel, flick, tap). - **Raised numbers** (c06603a, `<sup><font size="-1">`). Full-resolution crops in Sans (ASV Ps 119:139) and Serif (same verse) show the number under a "g" descender of the line above, clear of it in both fonts. Line pitch is logged as `running 24 verse 24` in Sans and `23/23` in Serif. - **Paragraphs were missing for BSB** (fixed in 3835f97). BSB Matt 13 in running mode was 3 rows for the whole window. BSB marks paragraphs as `<div type="x-p" sID=…/>` *inside* the pre-verse block after its headings, and the filter skipped that whole block and only knew `div type="paragraph"`. Now Matt 13 starts paragraphs at 1, 3, 5, 7–11, 14, 16, 18, 20, 22–24, 27–29, … (the module's own marks), and the window has 73 rows. KJV is unchanged (Gen 1: 1, 6, 9, 14, 24, 26, 29). - Tests: `filter::osis::paragraph_starts_come_before_the_first_text` (the real BSB 13:18 markup) and `reader::marks_paragraph_starts` (BSB Matt 13 has 18 and 24). Both fail without each half of the fix: without the pre-verse check, osis.rs:370; without the `x-p` div match, osis.rs:375. `cargo make` passes. - **Go to Matt 13:21** (mid-paragraph) puts 21's line at the top (saved 24545); **Back** returns to 13:16 (24540). **Kill and relaunch** restores BSB 24540 in running mode. - I found a separate bug in the ASV module, in both modes: words run together around added words ("allthyprecepts", "AndI"). The module itself has no space between `</w><transChange>`, so this is not caused by this PR. Filed as #119. The phone is back to Sans and verse mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-review left a comment

Review level: medium (re-review of fixes since 199e13d)
Commit: 3835f97b5f
Range: 199e13d..HEAD

Files reviewed:

  • core/src/bible/block.rs
  • core/src/bible/reader.rs
  • core/src/render.rs
  • docs/04-features.md
  • qml/pages/DisplaySettings.qml
  • qml/pages/ReadingPane.qml
  • sword/src/filter/osis.rs
  • tools/e2e/cases/running-text.sh
    (also read for context: core/src/bible/chapter.rs, tools/e2e/lib.sh, tools/golden/fetch.sh)

Findings: no high or medium defects found.

  • low · core/src/render.rs:6 · the module doc says a paragraph's inline numbers add "a <sup> and a styled <span>", but block.rs emits <sup><font size="-1" color><b>; no <span> exists, so the comment misleads the next reader (no runtime effect).
  • low · qml/pages/ReadingPane.qml:462 · the pitch is logged once per app run from the first paragraph row's Component.onCompleted. If pitchProbe.height is still 0 or layout is not final at that moment, the e2e awk check (v > 0) fails as a flake rather than a real regression. The failure scenario is timing-dependent and I could not confirm it.

Checked and fine: plain-text positions for verseStart/verseEnd are unchanged by the <sup> markup and the wrapping <div>, since at counts only the digits and the no-break space. The x-p sID detection inside Skip::Preverse is gated on !seen_text, and the eID milestone is correctly not a paragraph start. The Loader-based delegate keeps the verseSpan/posAt/paragraphs API that placeTop and topVerse use.

🤖 Generated with Claude Code

Review level: medium (re-review of fixes since 199e13d) Commit: 3835f97b5fceb87e366e24b956d80910f88b1502 Range: 199e13d..HEAD Files reviewed: - core/src/bible/block.rs - core/src/bible/reader.rs - core/src/render.rs - docs/04-features.md - qml/pages/DisplaySettings.qml - qml/pages/ReadingPane.qml - sword/src/filter/osis.rs - tools/e2e/cases/running-text.sh (also read for context: core/src/bible/chapter.rs, tools/e2e/lib.sh, tools/golden/fetch.sh) Findings: no high or medium defects found. - low · core/src/render.rs:6 · the module doc says a paragraph's inline numbers add "a `<sup>` and a styled `<span>`", but block.rs emits `<sup><font size="-1" color><b>`; no `<span>` exists, so the comment misleads the next reader (no runtime effect). - low · qml/pages/ReadingPane.qml:462 · the pitch is logged once per app run from the first paragraph row's Component.onCompleted. If pitchProbe.height is still 0 or layout is not final at that moment, the e2e awk check (`v > 0`) fails as a flake rather than a real regression. The failure scenario is timing-dependent and I could not confirm it. Checked and fine: plain-text positions for verseStart/verseEnd are unchanged by the `<sup>` markup and the wrapping `<div>`, since `at` counts only the digits and the no-break space. The x-p `sID` detection inside Skip::Preverse is gated on !seen_text, and the `eID` milestone is correctly not a paragraph start. The Loader-based delegate keeps the verseSpan/posAt/paragraphs API that placeTop and topVerse use. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Render: the module doc names the inline number's real markup (review 319)
Some checks failed
CI / changes (pull_request) Successful in 6s
CI / rust (pull_request) Successful in 3m34s
CI / audit (pull_request) Successful in 8s
CI / app (pull_request) Successful in 2m19s
CI / e2e (pull_request) Failing after 8m33s
CI / click (pull_request) Has been skipped
f8ba0c6387
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Collaborator

Replies to review 319 (medium, 3835f97, no high or medium findings):

  • low · core/src/render.rs:6, the doc says <span>: fixed in f8ba0c6. The comment now names <sup><font size="-1"><b>. It's a comment change only.
  • low · ReadingPane.qml:462, the pitch log may come too early: I'm keeping it as it is.
    • pitchProbe is a plain Text declared before the rows. Its height is computed synchronously from its font and lineHeight, with no layout pass.
    • The TextEdit's lineCount and contentHeight are computed synchronously when text is assigned, which happens before Component.onCompleted.
    • Every run so far logged a non-zero pair: e2e-docker on c06603a (22/22), and on the phone in Sans (24/24) and Serif (23/23). A zero would fail the case loudly instead of passing it silently.

🤖 Generated with Claude Code

Replies to review 319 (medium, 3835f97, no high or medium findings): - **low · core/src/render.rs:6, the doc says `<span>`:** fixed in f8ba0c6. The comment now names `<sup><font size="-1"><b>`. It's a comment change only. - **low · ReadingPane.qml:462, the pitch log may come too early:** I'm keeping it as it is. - `pitchProbe` is a plain `Text` declared before the rows. Its `height` is computed synchronously from its font and lineHeight, with no layout pass. - The `TextEdit`'s `lineCount` and `contentHeight` are computed synchronously when `text` is assigned, which happens before `Component.onCompleted`. - Every run so far logged a non-zero pair: e2e-docker on c06603a (`22/22`), and on the phone in Sans (`24/24`) and Serif (`23/23`). A zero would fail the case loudly instead of passing it silently. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Pane: the red-on-screen count reads the row's text again, behind the Loader (#12 part 3c)
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / rust (pull_request) Successful in 3m13s
CI / audit (pull_request) Successful in 8s
CI / app (pull_request) Successful in 2m16s
CI / e2e (pull_request) Successful in 8m29s
CI / click (pull_request) Successful in 1m12s
fb44892742
The delegate became a Loader in 70255da, so the list's children had no
text and the red-letters e2e case saw 0 red verses with red on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude-review left a comment

Level: medium (delta review). Commit: fb44892742. Range: f8ba0c6..HEAD. Blast radius: trunk. Testing tier: standard.

Files reviewed: qml/pages/ReadingPane.qml (+2 lines), plus its consumers: the redCount Timer (ReadingPane.qml:88-99), tools/e2e/cases/red-letters.sh, and the other delegate users (itemAtIndex at lines 306 and 337).

Checks:

  • The Timer reads items[i].text on verses.contentItem.children. It now finds the delegate's readonly property string text: model.text. Children that are not delegates (no string text) are skipped by the typeof guard, so they are not counted.
  • The text property on the delegate root does not shadow anything. Every use of the model role inside the delegate is the explicit model.text (lines 428-429 and 463). The other text: lines are assignments on child items (the number Text, the ornament and the break lines), and those resolve to their own properties. Nothing in the delegate reads a bare text.
  • itemAtIndex callers use other properties (paragraphs, textTop and similar), so they are unaffected.
  • red-letters.sh only reads the "utsb: red on screen N" log line, so its contract is unchanged.

Findings: none (no defects with a concrete failure scenario).

🤖 Generated with Claude Code

Level: medium (delta review). Commit: fb44892742cbe2ca966b1bf58cd4747670ce81e2. Range: f8ba0c6..HEAD. Blast radius: trunk. Testing tier: standard. Files reviewed: qml/pages/ReadingPane.qml (+2 lines), plus its consumers: the redCount Timer (ReadingPane.qml:88-99), tools/e2e/cases/red-letters.sh, and the other delegate users (itemAtIndex at lines 306 and 337). Checks: - The Timer reads `items[i].text` on `verses.contentItem.children`. It now finds the delegate's `readonly property string text: model.text`. Children that are not delegates (no string `text`) are skipped by the `typeof` guard, so they are not counted. - The `text` property on the delegate root does not shadow anything. Every use of the model role inside the delegate is the explicit `model.text` (lines 428-429 and 463). The other `text:` lines are assignments on child items (the number Text, the ornament and the break lines), and those resolve to their own properties. Nothing in the delegate reads a bare `text`. - itemAtIndex callers use other properties (`paragraphs`, `textTop` and similar), so they are unaffected. - red-letters.sh only reads the "utsb: red on screen N" log line, so its contract is unchanged. Findings: none (no defects with a concrete failure scenario). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-bot changed title from WIP: Running text: paragraphs of verses with inline numbers (#12 part 3c) to Running text: paragraphs of verses with inline numbers (#12 part 3c) 2026-10-29 15:27:03 +00:00
claude-review left a comment

claude-review merge gate, medium effort, head fb44892. Checked: (1) review 317's medium (ruler.text always true) was fixed in 6375bad and the ruler later removed; its low was fixed; 319's lows are fixed (f8ba0c6) or refuted (comment 2398). (2) Read both commits after 3835f97: f8ba0c6 is a doc-comment-only change in render.rs; fb44892 adds readonly property string text: model.text to the row delegate in ReadingPane.qml, restoring the red-count E2E hook behind the Loader. No findings. (3) Commit status on fb44892 is success (CI changes, rust, audit, app and the rest). (4) The PR Proof lists no unrun gate the user must decide on; standard tier, so no elevated gate. Supersedes review 317.

claude-review merge gate, medium effort, head fb44892. Checked: (1) review 317's medium (ruler.text always true) was fixed in 6375bad and the ruler later removed; its low was fixed; 319's lows are fixed (f8ba0c6) or refuted (comment 2398). (2) Read both commits after 3835f97: f8ba0c6 is a doc-comment-only change in render.rs; fb44892 adds `readonly property string text: model.text` to the row delegate in ReadingPane.qml, restoring the red-count E2E hook behind the Loader. No findings. (3) Commit status on fb44892 is success (CI changes, rust, audit, app and the rest). (4) The PR Proof lists no unrun gate the user must decide on; standard tier, so no elevated gate. Supersedes review 317.
schaefera deleted branch feat/running-text 2026-10-29 15:44:13 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
schaefera/utStudyBIble!115
No description provided.