Skip to content

fix: reopen the last text, widen the column, strengthen the highlight - #23

Merged
widgetii merged 1 commit into
mainfrom
fix/reader-defects
Oct 4, 2026
Merged

widgetii merged 1 commit into
mainfrom
fix/reader-defects

Conversation

@widgetii

@widgetii widgetii commented Oct 4, 2026

Copy link
Copy Markdown
Member

Three defects the side-by-side against the prototype turned up.

The library recorded which text was open and never read it back

Every launch asked the reader to choose a text with one text in the list. lastOpenedAt has been written since M2 and nothing consumed it.

Only a text that has actually been opened counts. Falling back to the newest import would open a document the reader has never seen, which is a guess rather than a restore. Restored once per launch rather than on every reload — a reload follows every import and delete, and re-selecting there would fight whatever the reader had just chosen.

The column was capped in points, which is the wrong unit

720 points gave 32 characters a line at 22pt and 15 characters at 48pt — a column two words wide for exactly the reader who enlarged the text in order to read it more easily.

The cap is now 40 characters, the top of the comfortable range for CJK, converted to points through the font size. That conversion works because a Han glyph is one em wide, which is the point of the square em.

Measured on the running app:

before after
22 pt 713 pt 876 pt
48 pt 713 pt (15 characters) 1067 pt, limited by the window

The revealed-word fill was too faint to read as a marker

At 0.08 it composites to 1.11:1 against white and 1.08:1 against the macOS dark background. Revealed is now 0.14 and selected 0.30 — 1.20:1 and 1.49:1.

I reported this one as a dark-mode defect and the measurement says otherwise: the two backgrounds differ by about 0.03, so one pair of values serves both, and a colorScheme parameter would be ceremony around a rounding error. The fill was simply too light everywhere.

Revealed stays deliberately quiet. The reading printed above the word is the primary signal that it is revealed; the fill only has to make the set scannable, and a page where a reader has revealed two hundred words should not turn into a wall of colour. Selection, which also carries a border, is the state that has to be unmistakable.

Testing

Six new tests, each pinning the thing that went wrong rather than the fix:

  • the column holds the same character count at every size, and a larger size gets a wider column
  • a revealed fill stays above a visibility floor, and below a loudness ceiling
  • the last-read text is found even when it is not the newest import; a library nothing has been read from restores nothing

All three verified on the running app, not only in tests: it now reopens 孔乙己 with no row clicked, and the column measurements above come from capturing the real window.

Local: 399 tests pass, lint clean, no warnings under -warnings-as-errors, both app bundles build.

🤖 Generated with Claude Code

Three defects the side-by-side against the prototype turned up.

**The library recorded which text was open and never read it back.** Every
launch asked the reader to choose a text with one text in the list.
`lastOpenedAt` has been written since M2 and nothing consumed it. Only a text
that has actually been opened counts: falling back to the newest import would
open a document the reader has never seen, which is a guess rather than a
restore. Restored once per launch, not on every reload, because a reload
follows every import and delete and would fight whatever the reader had just
chosen.

**The reading column was capped in points, which is the wrong unit.** 720
points gave 32 characters a line at 22pt and **15 characters at 48pt** — a
column two words wide for exactly the reader who enlarged the text in order
to read it more easily. The cap is now 40 characters, the top of the
comfortable range for CJK, converted to points through the font size, which
works because a Han glyph is one em wide. Measured on the running app: at
22pt the column goes from 713 to 876 points, and at 48pt from 713 to 1067,
where it is now limited by the window rather than by the constant.

**The revealed-word fill was too faint to read as a marker.** At 0.08 it
composites to a 1.11:1 contrast ratio against white and 1.08:1 against the
macOS dark background. Revealed is now 0.14 and selected 0.30, giving 1.20:1
and 1.49:1.

I had reported that one as a dark-mode defect, and the measurement says
otherwise: the two backgrounds differ by about 0.03, so one pair of values
serves both and a `colorScheme` parameter would be ceremony around a rounding
error. Revealed stays deliberately quiet — the reading printed above the word
is the primary signal, and a page with two hundred revealed words should not
become a wall of colour — while selection, which also carries a border, is
the state that has to be unmistakable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Restore the last-read text and improve reader layout and highlights

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Reopen the last-read text on launch without replacing later selections or opening unread imports.
• Size the reading column by character count so larger text has room to breathe.
• Strengthen revealed and selected highlights, with regression tests for all three defects.
Diagram

graph TD
  Store[("Library Store")] --> Model["Library Model"] --> Root["Root View"] --> Screen["Reader Screen"] --> Surface["Reader Surface"] --> Token["Token View"]
  Style["Reader Style"] --> Surface
  Highlight["Token Highlight"] --> Token
Loading
High-Level Assessment

The approach fits the existing architecture: reuse persisted last-opened metadata, derive column width from the resolved font size, and adjust the existing highlight resolver. Falling back to import order or adding theme-specific highlight rules would not address the observed defects as directly.

Files changed (9) +160 / -12

Bug fix (6) +77 / -12
DesignTokens.swiftRemove the fixed-point column cap +0/-3

Remove the fixed-point column cap

• Removes the 720-point reading-column limit so column sizing can be derived from the reader's font size instead.

Sources/HanReaderUI/Design/DesignTokens.swift

ReaderStyle.swiftDefine a font-relative column limit +22/-0

Define a font-relative column limit

• Adds a 40-character line limit and computes the maximum column width from the resolved font size.

Sources/HanReaderUI/Design/ReaderStyle.swift

TokenEmphasis.swiftIncrease revealed and selected fill visibility +26/-7

Increase revealed and selected fill visibility

• Raises normal and increased-contrast fill opacities while preserving a quieter revealed state and a stronger selected state. Documents the contrast measurements behind the shared light- and dark-theme values.

Sources/HanReaderUI/Design/TokenEmphasis.swift

HanReaderRootView.swiftRestore the last-read selection after library load +16/-1

Restore the last-read selection after library load

• Selects the most recently opened text after loading the library, but only when no text is already selected. State prevents restoration from repeating on subsequent view tasks.

Sources/HanReaderUI/HanReaderRootView.swift

LibraryModel.swiftFind the most recently opened library text +12/-0

Find the most recently opened library text

• Adds a computed property that selects the item with the latest last-opened timestamp. Unopened imports are excluded rather than treated as restoration candidates.

Sources/HanReaderUI/Models/LibraryModel.swift

ReaderSurface.swiftApply the font-relative column width +1/-1

Apply the font-relative column width

• Uses ReaderStyle's computed maximum width in place of the fixed 720-point cap.

Sources/HanReaderUI/Reader/ReaderSurface.swift

Tests (3) +83 / -0
LibraryModelTests.swiftCover last-read restoration candidates +42/-0

Cover last-read restoration candidates

• Tests that reading order wins over import order and that libraries with no opened text, including empty libraries, yield no restoration candidate.

Tests/HanReaderUITests/LibraryModelTests.swift

ReaderStyleTests.swiftGuard character-based column sizing +21/-0

Guard character-based column sizing

• Tests that the column cap represents the same character count at multiple font sizes and grows wider as the font size increases.

Tests/HanReaderUITests/ReaderStyleTests.swift

TokenHighlightTests.swiftBound highlight visibility and restraint +20/-0

Bound highlight visibility and restraint

• Tests minimum fill visibility for revealed and selected words and ensures revealed fill stays quieter than selected fill.

Tests/HanReaderUITests/TokenHighlightTests.swift

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@widgetii
widgetii merged commit 31041aa into main Oct 4, 2026
8 checks passed
@widgetii
widgetii deleted the fix/reader-defects branch October 4, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant