Add max_lines to :text so a name can stay on one line - #144
Open
asheehan wants to merge 1 commit into
Open
Conversation
A :text node wrapped without limit on both platforms; the only single-line text was a :button label, hard-coded to one line on each side. Apps that needed a merchant name beside a non-shrinking amount truncated the string by grapheme count in Elixir, which guesses at a width only native layout knows. max_lines: pos_integer() on :text. The renderer forwards it, drops nil so a conditional prop stays safe, and raises on anything else. iOS applies .lineLimit(n).truncationMode(.tail) only when set, so an unset node keeps its default wrap; MobNode carries it as an NSInteger with 0 as unset and the NIF reads it NSNumber-guarded. Mob.UI.text/1 keeps the prop through its allowlist. The Android reader lives in the generated MobBridge.kt (mob_new), landing separately; an older bridge ignores the key. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019tx3qSxxz11ijDcPZ9a4rN
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 freeto 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.
Closes#143.
Summary
A
:textnode wraps without limit on both platforms; the only single-line text is a:buttonlabel, hard-coded to one line on each side. Apps that needed a merchant name beside a non-shrinking amount truncated the string by grapheme count in Elixir, which guesses at a width only native layout knows.This adds
max_lines: pos_integer()on:text. Unset means what it meant before, so no existing tree renders differently.API
Mob.Rendererforwards a positive integer as"max_lines"; dropsnil(somax_lines: if(compact?, do: 1)is safe — a JSON null would reach iOS asNSNull); raisesArgumentErrorfor anything else. Validated in the renderer because it is the one point every construction path (map literal,Mob.UI.text/1,~MOB) passes through, and each platform would otherwise coerce a bad value into a different limit (Compose rejectsmaxLines <= 0).Mob.UI.text/1keeps the prop through its allowlist.Textdoes not; one mode both honour beats a prop that means different things per platform. Rationale indecisions/2026-09-07-max-lines-is-a-native-prop.md.Platform notes
iOS (this PR):
MobNodegainsNSInteger maxLines(0 = unset).mob_nif.maddsMOB_PROP_max_linesto the slot enum/names table and reads it NSNumber-guarded in the node builder.MobRootView.swift's.labelcase applies.lineLimit(n).truncationMode(.tail)through the existingifLet, so an unset node takes no modifier at all..buttonis untouched (still fixed at 1).Android (companion PR GenericJam/mob_new#62): the Compose renderer lives in the generated
MobBridge.kt, soMobTextreads the prop there —maxLines = n, overflow = TextOverflow.Ellipsis, falling back to Compose's own defaults (Int.MAX_VALUE,Clip) when absent. An app must be regenerated (or its bridge re-rendered) to pick it up; an older bridge ignores the key and wraps as before. The CHANGELOG entry says so.Test plan
Fidelity ladder, per AGENTS.md — stopped at rung 2:
mix format --check-formatted,mix credo --strict(ExSlop + Jump checks),mix compile --warnings-as-errors,xcrun clang-format --dry-run -Werroronios/mob_nif.m,MobNode.h,MobNode.m— all clean.swiftlintwas installed but cannot run in this environment (no Xcode SourceKit), so that rung was not run.mix test— 1559 passed, 38 excluded (baseline 1554 + 5 new). New tests: renderer dropsnil/ raises on0,-1,1.0,"1";Mob.UI.text/1keepsmax_lines; a source-contract test pins the iOS boundary (MobNode.h/.m, themob_nif.mslot and NSNumber guard, theifLet+lineLimitinMobRootView.swift) in the style ofnative_layout_weight_test.exs. Each was checked by reverting its half of the change: all fail. A plain "positive integer passes through" test was deliberately not added — the generic fallback already sent it before this clause existed, so it could not fail on revert.cond ? node.x : nil→ifLetshape already compiled at the.boxsizing site, so I am reasoning about its compile, not claiming it.Ran with Elixir 1.20.1 / OTP 28.3 (
.tool-versionspins 1.20.0-otp-29, which isn't installed here;mix.exsrequires~> 1.19).Pre-commit adversarial review by a separate agent found no blocking items; its one should-fix (the vacuous pass-through test) is removed. One observation from it worth a follow-up issue rather than a change here: sibling numeric reads in the node builder (
letter_spacing,line_height,weight) areif (x) [x doubleValue], so anilprop for those reachesNSNullunguarded via the generic fallback.max_linesguards for it; the others predate this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_019tx3qSxxz11ijDcPZ9a4rN