Configurable bullet shapes and task checkbox style - #150
Open
luisKisters wants to merge 7 commits into
Open
Conversation
Cut BulletShape.glyph, BulletStyle.sizeScale and BulletStyle.tiered — no embedder set them — and flatten the bullet draw switch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gap was a fixed 2pt tuned for the font-derived SF Symbol box. A larger configured box needs a larger gap or the label reads as touching it. Both the draw site and the click hit-test take it from the style, so their rects still cannot drift.
…stroke usesSystemSymbol inferred the rendering path from `self == .default`, so an embedder could not ask for the drawn box at default metrics and any future field would silently re-route embedders between the two paths. Make it an explicit `rendering` mode instead. Also lift the ring's hardcoded stroke width into the style, document the bullet-level attribute key, and compare the whole ListStyle for the redisplay check so a later knob cannot be missed. Tests pin the 0-based indentLevel to 1-based ladder conversion across a real nested list, plus the checkbox size and gap geometry.
Adds ListStyle.leadingIndent, .markerColumnWidth and .markerCenterOffset plus BulletStyle.diameter so an embedder can put the marker column on the paragraph margin and share one anchor between bullets and task checkboxes. The filled dot loses its glyph branch, so all depths share one diameter and the x-height midline; the checkbox centres on cap height and its hit-test follows the same geometry. Defaults reproduce the previous rendering. swift test: 258 tests, 46 suites, green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A pinned marker column measured a literal "- ", so `*` and `+` items put their content in a different column than `-` items. Measure the item's own marker plus its space instead. Bullet shapes also drew at unsnapped origins while the task checkbox already snapped to the device grid, so a small dot straddled two pixels. Snap the shape origin the same way; the triangle apex follows the snapped rect.
`recalcOverscroll` returned early whenever the measured content height and the resolved overscroll were both unchanged, but in `.scrolls` the managed frame height is `max(content, viewport - header)`. A viewport change alone therefore left a stale frame, and the text view could end up SHORTER than its clip view: the strip below the last line belongs to the container, so clicking it places no caret. Seen in NoteTakr: the first layout runs before the text view has a scroll view (viewport 0), where the policy resolves the same 39pt overscroll as at the real 170pt viewport, so nothing re-applied the frame and the body stayed 101pt tall inside a 170pt clip.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Bullet markers and task checkboxes are the only list rendering an embedder cannot influence today:
drawBulletMarkerspaints a hardcoded"•"intheme.bodyTextat every nesting depth, anddrawTaskCheckboxespaints the SF Symbolssquare/checkmark.square.fill.ListStylecarries indent and line-height knobs but nothing for the markers themselves.This adds two configuration groups so a host app can restyle both without forking.
API
BulletStyle.shapeLadderis indexed by 1-based nesting depth; the last entry repeats for deeper levels, and an empty ladder falls back to.filledDot. Shapes other than.filledDotareNSBezierPath, so they stay crisp at any scale.Defaults are unchanged
BulletStyle.defaultis[.filledDot], which keeps the exact existing"•"NSString draw — same font, same centring within the marker advance.TaskCheckboxStyle.defaultis.systemSymbol, which keeps the existing SF Symbol branch untouched. All 253 existing tests pass with no edits to them.renderingis an explicit mode rather than an inferredself == .default, so adding a field later cannot silently re-route existing embedders between the two paths.How depth reaches the renderer
MarkdownASTStyler.styleListItemalready computesMarkdownLists.indentLevel(from:)for its indent. It now also writes that depth (1-based) onto the marker range as abulletListLevelattribute, following the existingblockquoteLevelprecedent.drawBulletMarkersreads it and defaults to depth 1 when absent. Both attributes are written together and cleared together by the existing whole-paragraphsetAttributes, so a stale depth cannot survive an edit.Draw / hit-test parity
TaskCheckboxGeometrystays the single source of truth.size(for:style:)andboxX(contentX:size:gap:)are used by bothMarkdownTextLayoutFragment.drawTaskCheckboxesandNativeTextView+TaskCheckbox.taskCheckboxHit, so a configured size or gap moves the drawn box and the click target together.Tests
Added: default-preservation for both styles, ladder clamping, the 0-based
indentLevelto 1-based ladder conversion across a real nested list, and the checkbox size and gap geometry. 255 tests pass.One thing worth your call:
MarkdownLists.indentLevel(from:)is a whitespace heuristic (tabs plus spaces / 2) that does not consultlists.indentPerLevel, so 4-space-indented CommonMark resolves one level deeper than it looks. That is pre-existing behaviour and this PR does not touch it, but the shape ladder makes it visible for the first time.