#1

DiffsView renders at 0 height in content-sized parents (workaround for mrousavy/nitro#1199)

Openyyq1025opened 6mo ago

Summary

DiffsView requires its parent to supply explicit dimensions (typically flex: 1 or a fixed height). When embedded in a content-sized parent — e.g. a chat bubble or a <View> with no flex — it renders at 0 height and content is invisible.

Reproduce

<View style={{ padding: 16 }}>
  <DiffsView content="```ts\nconst x = 1;\n```" colorScheme="light" />
</View>

Expected: bubble grows to fit the rendered code block. Actual: bubble has 0 height.

Root cause

Nitro 0.35 does not propagate the underlying UIView's intrinsic size to Yoga. Confirmed by reading the nitrogen output:

  • nitrogen/generated/shared/c++/views/HybridDiffsComponent.hpp declares the shadow node as a plain ConcreteViewShadowNode<...>, without the MeasurableYogaNode / LeafYogaNode traits.
  • Yoga's LayoutableShadowNode::measureContent default returns zero, and is only consulted when MeasurableYogaNode is set on the shadow node traits (react-native YogaLayoutableShadowNode.cpp).

The framework-level fix is tracked at mrousavy/nitro#1199 — Support measureContent(...) for Nitro Views, opened by Marc Rousavy himself. As of writing it has 0 comments and no implementation.

Until Nitro adds measureContent, every Nitro view has the same limitation. The pragmatic mitigation at the consumer-library level is to surface the measured height through a callback so the JS parent can apply it as inline style.height.

Use case

Embedding DiffsView in a chat-style transcript: one Claude assistant turn per "bubble", each bubble's height driven by the rendered markdown. flex: 1 is not applicable because bubbles must be content-sized so they stack and the parent ScrollView can scroll across multiple turns.

Proposed solution

Add an optional callback prop to DiffsView:

export interface ContentSize {
  width: number;
  height: number;
}

export interface DiffsProps extends HybridViewProps {
  // ... existing props
  onContentSizeChange?: (size: ContentSize) => void;
}

Implementation outline (iOS):

  • Outer view becomes a small UIView subclass that fires a callback on layoutSubviews bounds change.
  • After setMarkdown (switched to setMarkdownManually for synchronous Core Text layout), call markdownTextView.boundingSize(for: width) and invoke the callback.
  • Coalesce same-size reports to avoid feedback loops when JS reflows on each report.

Caller pattern (sidecode wrapper, simplified):

function MarkdownView(props) {
  const [height, setHeight] = useState<number>();
  return (
    <DiffsView
      {...props}
      onContentSizeChange={{ f: (size) => setHeight(size.height) }}
      style={{ height }}
    />
  );
}

Backward compatible: existing flex: 1 consumers (e.g. v0.app/ios) are not affected; the new prop is opt-in.

Next step

Happy to submit a PR with the spec change, iOS implementation, and an updated example/ showcasing the chat-bubble pattern. Reference the work I've already done in a private fork — full diff is small (~50 LOC excluding nitrogen-generated output). Would you accept this approach?

Activity

0

No activity yet.