Skip to content

Customizable Gutter#105

Open
romanr wants to merge 21 commits intokrzyzanowskim:mainfrom
romanr:gutter-mutter
Open

Customizable Gutter#105
romanr wants to merge 21 commits intokrzyzanowskim:mainfrom
romanr:gutter-mutter

Conversation

@romanr
Copy link

@romanr romanr commented Mar 6, 2026

Adds a new gutter system that lets consumers supply a custom NSView for each visible line, independent of the built-in line number gutter. Designed for use cases like breakpoint indicators, diagnostic markers, or code folding controls.

image

AppKit layer (STTextView.swift, STTextView+Gutter.swift):

  • New properties on STTextView: customGutterWidth, gutterLineViewProvider, customGutterBackgroundColor, customGutterSeparatorColor, customGutterSeparatorWidth, customGutterContainerView
  • layoutGutter() now runs custom gutter layout alongside (but independent of) the built-in gutter
  • Custom gutter uses a floating container view (STCustomGutterContainerView) — flipped, non-opaque, consumes mouse events to prevent click-through
  • One view per visible paragraph, positioned to first visual line height (handles wrapped lines correctly)
  • Content offset (contentView.frame.origin.x) falls back to customGutterWidth when no built-in gutter is present
  • Trailing separator with configurable color and width

SwiftUI layer (TextView.swift):

  • New TextViewWithGutter<GutterContent> view with @ViewBuilder API — wraps SwiftUI views in NSHostingView and passes through as the provider closure
  • Environment-based style modifiers: .gutterBackground(_:), .gutterSeparator(color:width:)
  • TextViewRepresentable extended with gutter properties, wired through makeNSView/updateNSView

romanr and others added 16 commits March 5, 2026 13:00
Keep TextView's public init clean (text, selection, options, plugins only).
Introduce TextViewWithGutter<GutterContent> as a dedicated view type for
editors with custom per-line gutters. Gutter styling (background color,
separator) uses environment-based view modifiers (.gutterBackground,
.gutterSeparator) following the library's existing TextViewModifier pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Demonstrates TextViewWithGutter API with per-line content:
- Word count display (instead of line numbers)
- Toggleable bookmark icon (bookmark/bookmark.fill)
- Overhanging breakpoint badge with shadow (activated by tapping number)
- Gutter styling via .gutterBackground() and .gutterSeparator() modifiers

Activated via toolbar toggle button (list.star icon).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove clipsToBounds from container to allow content overhang
  (e.g. breakpoint badges with shadows extending past gutter edge)
- Remove CATiledLayer — use regular CALayer instead. CATiledLayer
  renders asynchronously which breaks NSHostingView event delivery
- Add subview to container BEFORE setting frame so NSHostingView
  has a window and can properly lay out SwiftUI content
- Use identifier-based view management instead of remove-all/recreate
  pattern — line views are tagged and pruned by visibility
- Separator uses its own identifier to avoid being pruned with line views

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace closure properties (onToggleBookmark, onToggleBreakpoint) with
@binding parameters in CustomGutterLineView. The preview thunking system
wraps expressions in __designTimeSelection<T> which can't reconcile
() -> Void vs @mainactor () -> Void from SwiftUI View's actor isolation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use BreakpointShape (Union path from Figma) instead of RoundedRectangle
  for breakpoint badge — arrow-right tab shape matching Xcode breakpoints
- Increase bookmark icon from 9pt to 11pt to match design proportions
- Breakpoint badge replaces entire gutter row content (not alongside bookmark)
- Fix separator width: 2pt to match Figma border-r-2
- Fix word count font weight: .medium (was .regular) to match SF Pro Rounded
- Add XCLocalSwiftPackageReference to project.pbxproj so clean builds
  can resolve STTextView package dependencies (STTextKitPlus, CoreTextSwift)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use fragmentView.frame directly for custom gutter line positioning
instead of cellFrame from STGutterCalculations — the latter adds
typographicBounds.origin.y offset designed for baseline-aligned
line numbers, which shifts NSHostingView content down.

Place gutter separator behind line views so overhanging content
(breakpoint badges) draws in front of the separator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use fixed-width frame on word count label so the bookmark icon
stays in the same position regardless of whether a count is shown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update Union shape to new Figma export sized at 28×15
- Bookmark icon stays visible when breakpoint is active
- Number stays in same position (same font/frame as word count)
- Badge shape extends rightward past gutter separator
- HStack spacing matches Figma gap (6px)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Badge no longer extends past the gutter separator. This ensures
consistent rendering in both Xcode previews and the live app,
avoiding clipping differences between environments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Offset badge by 4pt to compensate trailing padding, placing the
pointed tip at the separator line without crossing it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set clipsToBounds=false on gutter line views so overhanging content
renders consistently in both Xcode previews and the live app.
Restores the badge overhang that demonstrates custom gutter content
can extend beyond gutter bounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
STCustomGutterContainerView was missing the FB21059465 workaround
that STGutterView already applies: when NSScrollView has automatic
content insets (e.g. a toolbar), horizontal floating subviews do not
respect those insets and render at the wrong vertical position. Adding
the same layout() override shifts frame.origin.y by -topContentInset
so the custom gutter aligns with the text lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a paragraph wraps across multiple lines, the gutter line view
was sized to fragmentView.frame.size.height (the entire paragraph
height). NSHostingView is non-flipped, so inside the flipped gutter
container its y=0 is at the frame bottom — SwiftUI content rendered
anchored to the bottom of the tall frame instead of the top.

Fix: size each line view to textLineFragment.typographicBounds.height
(just the first visual line) so NSHostingView content aligns to the
top where the line begins. For extra line fragments where
typographicBounds.height may be invalid (FB15131180), fall back to
the previous line fragment's height or typingLineHeight.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without mouse event overrides, clicks in the gutter that land on the
container itself (or in gaps between line views) propagated up the
responder chain and reached STTextView, moving the insertion point and
highlighting lines. Override mouseDown/mouseDragged/mouseUp to consume
events — interactive SwiftUI subviews still receive their own events
via normal hit-testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…better flexibility and integration with SwiftUI
Copilot AI review requested due to automatic review settings March 6, 2026 18:15
@CLAassistant
Copy link

CLAassistant commented Mar 6, 2026

CLA assistant check
All committers have signed the CLA.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “custom gutter” system that lets consumers supply per-visible-line gutter views (AppKit via a data source, SwiftUI via a @ViewBuilder wrapper), independent of the built-in line number gutter.

Changes:

  • Introduces AppKit custom gutter plumbing on STTextView (width, data source, container, colors, separator) and layouts custom gutter views during gutter layout.
  • Adds SwiftUI TextViewWithGutter plus environment-driven style modifiers and a coordinator-kept data source adapter.
  • Updates the SwiftUI sample app to toggle between built-in line numbers and the custom gutter demo UI.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
TextEdit.SwiftUI/TextEdit.SwiftUI.xcodeproj/project.pbxproj Adds local Swift package reference and adjusts signing settings for the sample project.
TextEdit.SwiftUI/ContentView.swift Demo UI for toggling built-in line numbers vs custom gutter and per-line gutter content.
Sources/STTextViewSwiftUIAppKit/TextView.swift Adds TextViewWithGutter, gutter environment keys/modifiers, and representable wiring to AppKit.
Sources/STTextViewAppKit/STTextView.swift Adds custom gutter properties and uses custom gutter width for content offset/content sizing in some paths.
Sources/STTextViewAppKit/STTextView+Gutter.swift Runs custom gutter layout alongside built-in gutter; implements floating container + per-line view creation/pruning + separator.
Sources/STTextViewAppKit/STGutterLineViewDataSource.swift New protocol for providing per-line gutter views.
Sources/STTextViewAppKit/Gutter/STGutterView.swift Makes delegate publicly settable.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

romanr and others added 3 commits March 7, 2026 07:41
… add cleanup

- Fix updateTextContainerSize, intrinsicContentSize, and sizeToFit to
  fall back to customGutterWidth (consistent with setFrameSize and
  updateContentSizeIfNeeded)
- Scope gutterBackground/gutterSeparator modifiers to TextViewWithGutter
  so they don't appear on plain TextView
- Clear custom gutter state in updateNSView when gutter is disabled
- Fix misleading doc comment on layoutCustomGutterLineViews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TextViewEnvironmentModifier conforms to TextViewModifier, not
TextViewWithGutter, so chaining .gutterBackground().gutterSeparator()
breaks when modifiers are scoped to TextViewWithGutter only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 8 tests for custom gutter sizing: sizeToFit, intrinsicContentSize,
  consistency, cleanup, long content, data source queries, and container
  lifecycle (addresses PR review comment on missing test coverage)
- Fix doc comment on customGutterWidth referencing renamed property
  gutterLineViewProvider → gutterLineViewDataSource

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Author

@romanr romanr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review comments implemented/resolved

romanr added 2 commits March 8, 2026 02:41
typographicBounds.height only includes font metrics (~17pt), not
lineSpacing. This caused gutter labels to be half the visual line
height, misaligning them with text content. Using fragmentView.frame
height includes lineSpacing for correct 1:1 alignment.
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.

3 participants