You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
macOS: TextField text sits 2pt higher than NSTextField when not editing, and jumps down on focus #11661
On macOS, a TextField that is not being edited draws its text 2pt higher than a stock AppKit NSTextField. When the field gains focus, AppKit's field editor draws the text at the stock position, so the text jumps down 2pt. A Text label is not affected.
Measured
Borderless fields, 40pt tall, 16pt Helvetica. Distance from the field's top edge to the top of the "H":
Not editing
Editing
Stock AppKit NSTextField
3.5pt
3.5pt
Perry TextField
1.5pt
3.5pt
A Perry Text label and a stock NSTextField(labelWithString:) both put the "H" 3.5pt below the frame's top, so labels match.
The offset shows with zero insets, so it does not come from setPadding. Perry replaces each field's cell with PerryInsetTextFieldCell (install_text_field_cell in crates/perry-ui-macos/src/widgets/padding.rs), and the difference is in how that cell draws text when the field is not editing.
Repro
import{App,TextField,VStack,textSetFontFamily,textfieldFocus,textfieldSetBackgroundColor,textfieldSetBorderless,textfieldSetFontSize,textfieldSetString,textfieldSetTextColor,widgetSetBackgroundColor,widgetSetHeight}from"perry/ui"functionfield(text: string){constf=TextField("",()=>{})textfieldSetBorderless(f,1)textfieldSetFontSize(f,16)textSetFontFamily(f,"Helvetica")textfieldSetBackgroundColor(f,1,1,0.7,1)textfieldSetTextColor(f,0,0,0,1)widgetSetHeight(f,40)textfieldSetString(f,text)returnf}constfirst=field("Hxg first")constsecond=field("Hxg second")constbody=VStack(40,[first,second])widgetSetBackgroundColor(body,1,1,1,1)App({title: "probe",width: 400,height: 200,windowState: "normal", body })
The first field opens as the first responder, so it draws through the field editor. Its text sits 2pt lower in its box than the second field's text. Click into the second field and its text drops 2pt.
For the stock comparison, the same two fields built in Swift (NSTextField(frame:), isBezeled = false, isBordered = false, 16pt Helvetica, 40pt tall) draw the "H" 3.5pt below the top whether or not the field is editing.
Expected
A TextField draws its text in the same place whether or not it is editing, matching a stock NSTextField.
Perry: main at 4a1b788 with the sRGB change from #11613 applied, which touches only colour creation. macOS 26.6.2.
I dug into the root cause. It goes back to how we added padding to text fields, and it connects a string of earlier issues.
A stock NSTextField(string:) (textFieldWithString:) is already a one-line field. Its cell is scrollable, does not wrap, clips, and does not use single-line mode. Borderless, in a 60pt frame, with Helvetica at 10, 16 and 28pt, it draws the idle text where the field editor draws it (2.5, 3.5 and 7.5pt from the top). Long values stay on one line and scroll.
This issue comes from usesSingleLineMode. In that mode AppKit draws idle text on the baseline of the system font for the control size (13pt at regular size), whatever the cell's font is. The field editor uses the real font. So the text moves on focus: 2pt high at 16pt Helvetica, 3pt low at 10pt, and clipped at the top from about 20pt.
Each fix made sense on its own. Together they say the padding approach needs a rethink. #11665 fixes this issue by turning single-line mode off while the cell draws, which adds one more layer. I'll leave it as a draft and rework it.
Proposed approach: let AppKit build our cell. NSTextField creates its cell from the cellClass class property. A small NSTextField subclass whose cellClass returns the inset cell keeps both the custom cell and the factory's one-line setup. It draws correctly at every size above. That removes the cell swap, the property restore in install_label_cell, and the single-line properties from #10157. The inset cell keeps only its padding methods.
Whatever the new padding approach is, it has to keep all of these working:
The native tests in perry-ui-macos cover padding and label colour. #11665 adds one for this issue.
One behaviour change: without single-line mode, Option-Return inserts a newline, the same as in a stock NSTextField. TextArea is the multi-line widget, so that seems right to me.
The cell.PerryTextField and PerrySecureTextField return the inset cell from cellClass, so textFieldWithString: and labelWithString: build it with the factory setup. The cell swap, the property restore in install_label_cell, and the single-line properties from fix(ui-macos): keep editable TextField single-line (#10155) #10157 are gone.
Padding.setPadding on a text field now works as CSS padding does. The cell pads the frames AppKit hands it to draw and to edit, not drawingRectForBounds:. A bezeled or bordered field no longer moves its text by the padding on focus. textfieldSetBackgroundColor paints the field's layer, so the background fills the padding.
One line. Without single-line mode, a stock NSTextField keeps pasted and Option-Return newlines, and the field grows taller. So the field keeps the value to one line itself, with the same rules as a web <input> in Chrome. A value set in code drops its line breaks. Typed, pasted and dropped text turns each line break into one space. Option-Return and Control-Return insert nothing.
The checklist in my last comment holds. native_textfield_cell covers the baseline, one line, and padding in every border style. native_textfield_newlines covers line breaks.
Single-line mode is working as Apple documents it
This reproduces with no Perry code: a stock NSTextField with usesSingleLineMode = true and a non-system font jumps on focus, with the same numbers as the issue. Apple's reference for usesSingleLineMode says a single-line cell uses a fixed baseline that depends only on the control size, not on the font. In the idle draw, AppKit gets that baseline from -[NSFont baseFontForSingleLineModeCell:]. It returns the system font for the control size (13pt regular, 11pt small) unless the field already uses the system font. The field editor lays out with the field's own font, so the two disagree.
Others have hit the same thing and landed on the same fix:
Lazarus #39959 and MR !114 (2022) saw the offset with non-system fonts. They turned single-line mode off and removed line breaks in textView:shouldChangeTextInRange:replacementString:.
rdar://13813516 (2013) reported the clipping at 21pt and above with single-line mode on.
A Stack Overflow question saw the clipping with HelveticaNeue-UltraLight at 24pt and turned single-line mode off.
Activity
fix(ui-macos): drop the unused secure-cell draw override (PerryTS#11661)
steinybot commentedon Sep 29, 2026
I dug into the root cause. It goes back to how we added padding to text fields, and it connects a string of earlier issues.
A stock
NSTextField(string:)(textFieldWithString:) is already a one-line field. Its cell is scrollable, does not wrap, clips, and does not use single-line mode. Borderless, in a 60pt frame, with Helvetica at 10, 16 and 28pt, it draws the idle text where the field editor draws it (2.5, 3.5 and 7.5pt from the top). Long values stay on one line and scroll.How we got here:
TextFieldandSecureField. It swaps the cell AppKit builds for a newPerryInsetTextFieldCell(install_text_field_cellincrates/perry-ui-macos/src/widgets/padding.rs). A new cell does not keep the factory setup, so it wraps, does not scroll, and is not editable.textFieldWithString:. That diagnosis was wrong. The factory cell does not wrap. The swapped cell does. My fix in fix(ui-macos): keep editable TextField single-line (#10155) #10157 set the one-line properties again by hand, and addedusesSingleLineMode, which the factory does not set.Textlabels with the same swap. It then restores the label's properties one at a time (install_label_cell).textSetColordid nothing. My fix in fix(ui-macos): textSetColor no longer a no-op on a Text label #10862 changed the restore.usesSingleLineMode. In that mode AppKit draws idle text on the baseline of the system font for the control size (13pt at regular size), whatever the cell's font is. The field editor uses the real font. So the text moves on focus: 2pt high at 16pt Helvetica, 3pt low at 10pt, and clipped at the top from about 20pt.Each fix made sense on its own. Together they say the padding approach needs a rethink. #11665 fixes this issue by turning single-line mode off while the cell draws, which adds one more layer. I'll leave it as a draft and rework it.
Proposed approach: let AppKit build our cell.
NSTextFieldcreates its cell from thecellClassclass property. A smallNSTextFieldsubclass whosecellClassreturns the inset cell keeps both the custom cell and the factory's one-line setup. It draws correctly at every size above. That removes the cell swap, the property restore ininstall_label_cell, and the single-line properties from #10157. The inset cell keeps only its padding methods.Whatever the new padding approach is, it has to keep all of these working:
setPaddinginsetsTextField,SecureFieldandTextcontent (fix(perry/ui): padding dropped on non-stack widgets on Apple backends (macOS/iOS/tvOS/visionOS) #9954, macOS setPadding is implemented for only 4 widget classes and silently no-ops on the rest (Button, labels, etc.) #10159).TextFieldstays on one line and scrolls (macOS TextField wraps to multiple lines instead of staying single-line (diverges from iOS/GTK4/Android/Windows) #10155).textSetColorcolours aTextlabel (macOS: textSetColor is a no-op on a Text label (padding cell bakes attributedStringValue, overriding setTextColor:) #10856).TextFieldtext stays in place on focus, with any font and size (macOS: TextField text sits 2pt higher than NSTextField when not editing, and jumps down on focus #11661).The native tests in
perry-ui-macoscover padding and label colour. #11665 adds one for this issue.One behaviour change: without single-line mode, Option-Return inserts a newline, the same as in a stock
NSTextField.TextAreais the multi-line widget, so that seems right to me.fix(ui-macos): build the inset cell through cellClass (PerryTS#11661)
test(ui-macos): describe the label-colour constraint without the remo…
test(ui-macos): cover padding, one-line, secure and label colour in t…
refactor(ui-macos): rename the field classes to PerryTextField and Pe…
fix(ui-macos): pass layer colours as a typed CGColor pointer (PerryTS…
fix(ui-macos): pad text fields as CSS padding does (PerryTS#11661)
fix(ui-macos): keep TextField and SecureField to one line, as a web i…
steinybot commentedon Sep 30, 2026
The rework is in #11665.
PerryTextFieldandPerrySecureTextFieldreturn the inset cell fromcellClass, sotextFieldWithString:andlabelWithString:build it with the factory setup. The cell swap, the property restore ininstall_label_cell, and the single-line properties from fix(ui-macos): keep editable TextField single-line (#10155) #10157 are gone.setPaddingon a text field now works as CSS padding does. The cell pads the frames AppKit hands it to draw and to edit, notdrawingRectForBounds:. A bezeled or bordered field no longer moves its text by the padding on focus.textfieldSetBackgroundColorpaints the field's layer, so the background fills the padding.NSTextFieldkeeps pasted and Option-Return newlines, and the field grows taller. So the field keeps the value to one line itself, with the same rules as a web<input>in Chrome. A value set in code drops its line breaks. Typed, pasted and dropped text turns each line break into one space. Option-Return and Control-Return insert nothing.The checklist in my last comment holds.
native_textfield_cellcovers the baseline, one line, and padding in every border style.native_textfield_newlinescovers line breaks.Single-line mode is working as Apple documents it
This reproduces with no Perry code: a stock
NSTextFieldwithusesSingleLineMode = trueand a non-system font jumps on focus, with the same numbers as the issue. Apple's reference forusesSingleLineModesays a single-line cell uses a fixed baseline that depends only on the control size, not on the font. In the idle draw, AppKit gets that baseline from-[NSFont baseFontForSingleLineModeCell:]. It returns the system font for the control size (13pt regular, 11pt small) unless the field already uses the system font. The field editor lays out with the field's own font, so the two disagree.Others have hit the same thing and landed on the same fix:
textView:shouldChangeTextInRange:replacementString:.refactor(ui-macos): move the field classes next to their widgets (Per…
fix(ui-macos): keep line breaks in Text labels (PerryTS#11661)
refactor(ui-macos): state unused alternatives as hypotheticals in com…
fix(ui-macos): draw idle TextField text on its font's baseline (Perry…
test(ui-macos): add a smoke test for TextField baseline (PerryTS#11661)
18 remaining items
fix(ui-macos): drop the unused secure-cell draw override (#11661)
fix(ui-macos): build the inset cell through cellClass (#11661)
test(ui-macos): describe the label-colour constraint without the remo…
test(ui-macos): cover padding, one-line, secure and label colour in t…
refactor(ui-macos): rename the field classes to PerryTextField and Pe…
fix(ui-macos): pass layer colours as a typed CGColor pointer (#11661)
fix(ui-macos): pad text fields as CSS padding does (#11661)
fix(ui-macos): keep TextField and SecureField to one line, as a web i…
refactor(ui-macos): move the field classes next to their widgets (#11661
fix(ui-macos): keep line breaks in Text labels (#11661)
refactor(ui-macos): state unused alternatives as hypotheticals in com…
test(ui-macos): build the newlines test's closures from JsFunctionInfo (
refactor(ui-macos): name the line-break helpers for what they do (#11661
test(ui-macos): use generic text for the long TextField value (#11661)
style(ui-macos): rustfmt the newlines test's submit closure (#11661)