Skip to content

[Bug]: paragraph/heading blocks always report full offered width, breaking width-hugging containers #157

Description

Summary

SingleBlockView's .paragraph/.heading cases wrap ParagraphView in HStack(spacing: 0) { ParagraphView(...); Spacer() }. the trailing Spacer() has no minLength, so it absorbs all leftover width from whatever the HStack is proposed — meaning the block always reports the full proposed width upward, never the text's actual needed width. this breaks any ancestor that tries to hug its content to a capped width (the standard SwiftUI pattern for chat bubbles: propose a bounded max width, let the child report back its true smaller size). confirmed on both iOS (UIKit ParagraphUIView) and macOS (AppKit ParagraphNSView) — both set left-alignment intrinsically on the text itself (textAlignment/paragraphStyle.alignment), so it's not a platform-specific rendering path issue.

update: the same root cause also affects plain, non-hugging usage. because the HStack's space negotiation between ParagraphView and the unqualified Spacer() constrains the width actually proposed to ParagraphView for text layout, paragraphs wrap one or more words earlier than the space available to them — even with no capped/hugging ancestor at all. re-recording the library's own existing snapshot fixtures after the fix (testCitations, testCustomBlockSpacing, testMarkdownLists_uikit) shows the same short paragraph now fitting on fewer, wider lines, using the full available width instead of wrapping early. text content, formatting, and left-alignment are unaffected — only where lines break. so this bug isn't only a "chat bubble" edge case: it wastes horizontal space in the library's default, full-bleed rendering too.

Minimal markdown input

hi

Platform

iOS

(also reproduces on macOS — the dropdown above doesn't have a macOS option yet, but the root cause and fix are platform-agnostic: SingleBlockView in Sources/MarkdownText/UI/BlockView.swift is shared code.)

Expected behavior

when a paragraph or heading block is proposed more width than its text needs (e.g. by a chat-bubble container capping itself at 80% of screen width), the block reports back a width close to what the text actually needs, so the ancestor can shrink to fit — same as a one-word reply hugging tightly instead of stretching to the cap. separately, text should wrap using the full width actually available to it, not a narrower width caused by competing with the Spacer() for layout priority.

Actual behavior

the block always reports the full proposed width, regardless of how short the text is. a one-word paragraph proposed 300pt reports back 300pt, not ~40pt. any consumer relying on width-hugging (e.g. chat bubbles) gets a background/container that's always as wide as the cap, even for trivial replies. separately (and independent of any hugging ancestor), paragraphs wrap earlier than necessary — e.g. "This is a paragraph with some text to render." wraps onto two lines instead of fitting on one, even at full device width.

Validation

  • reproduced and root-caused directly in Sources/MarkdownText/UI/BlockView.swift: the unqualified Spacer() in the .paragraph/.heading HStack is the sole cause — ParagraphView's own left-alignment comes from the text itself, not from this wrapper, and BlockView's outer VStack is already alignment: .leading.
  • confirmed downstream in a real consumer (Threadwire, a chat SDK) via a local SPM package override: short replies fill to the bubble's width cap instead of hugging; long, wrapping content is unaffected.
  • confirmed against the library's own existing snapshot suite: re-recording testCitations, testCustomBlockSpacing, and testMarkdownLists_uikit after the fix shows only line-wrap-position changes (more words per line, fewer lines) — text, formatting, and alignment are pixel-identical otherwise.
  • have a fix ready (drop the HStack/Spacer wrapper, render ParagraphView directly) — happy to open a PR with test coverage once this is filed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    need_more_infoNeed more information from issue creator.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions